diff --git a/immortalwrt-patches/README.md b/immortalwrt-patches/README.md new file mode 100644 index 0000000..f276f3f --- /dev/null +++ b/immortalwrt-patches/README.md @@ -0,0 +1,36 @@ +# v33 完美固件源码包 + +对应 sysupgrade-v33.bin(md5 57d43f1a56c66fafd5c62e8846641232)。 + +## 包含文件(相对 ImmortalWrt 源码树根) + +### package/mtk/ — MTK 闭源驱动层补丁 +- `applications/mtwifi-cfg/files/mtwifi-cfg/mtwifi_cfg` — 5G/HE160 双分支写 VHT_Sec80_Channel=155 +- `applications/mtwifi-cfg/files/mtwifi.sh` — detect_mtwifi 诚实 UCI(idx=1=5g/HE160, idx=2=2g/HE40) +- `applications/mtwifi-cfg/files/netifd/mtwifi.sh` — v33 关键补丁:setup 完成后 deferred 后台检测 dat MacAddress 与物理 netdev MAC,重发 wireless_add_vif 上报物理 ifname +- `applications/luci-app-mtk/luasrc/controller/mtkwifi.lua` — 顶部空 index() 禁用 MTK 私有面板 + +### package/mtk/drivers/wifi-profile/ +- `files/mt7981/l1profile.dat` — 主次顺序 b0;b1(出厂值,未改动) + +### feeds/luci/ +- `modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js` — Overview/Modal SSID 读 iwinfo、reload-block 补丁、freqlist/txpowerlist 改传物理 ifname + +### files/ — 烤入 squashfs 的配置与脚本 +- `etc/uci-defaults/99-custom-config` — 首启 UCI 默认值(诚实标签 + turboacc tcpcca=bbr) +- `etc/wifi-band-verify.sh` — log-only 验证脚本 +- `etc/rc.local` — 开机兜底(检测 vxlan0,缺失重触发) +- `etc/hotplug.d/iface/30-mss-clamp` — MSS clamp 持久化 +- `etc/hotplug.d/iface/40-wg-track` — WG 隧道状态跟踪 +- `etc/sysctl.d/99-bridge-nf.conf` — bridge-nf-call-{ip,ip6,ar}tables=1 +- `etc/config/{wireless,firewall,dhcp,turboacc,dropbear,network}` — 7 个核心 UCI 配置全部烤入(**network 已脱敏:private_key 与 preshared_key 替换为占位符,使用前必须重新生成 WG 密钥对**) + +### 顶层 +- `netifd-wireless.sh` — `/lib/netifd/netifd-wireless.sh` (wifi-scripts 包) + +## 刷入流程 +1. 克隆 padavanonly fork 的 ImmortalWrt-Actions 源码树到 build server +2. 用本包覆盖对应路径 +3. `make -j4` → `bin/targets/mediatek/filogic/immortalwrt-mediatek-filogic-cudy_tr3000-v1-ubootmod-squashfs-sysupgrade.bin` +4. FIT 镜像雕刻验证:`OFF=$(grep -abo "hsqs" image.bin | head -1 | cut -d: -f1); dd if=image.bin of=root.sqfs bs=1 skip=$OFF; unsquashfs -d sq root.sqfs` +5. U-Boot TFTP/网页刷入(**绝不用 sysupgrade**) \ No newline at end of file diff --git a/immortalwrt-patches/feeds/luci/modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js b/immortalwrt-patches/feeds/luci/modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js new file mode 100644 index 0000000..0937491 --- /dev/null +++ b/immortalwrt-patches/feeds/luci/modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js @@ -0,0 +1,2468 @@ +'use strict'; +'require view'; +'require dom'; +require 'request'; + +var IW_ESSID = {}; +'require poll'; +'require fs'; +'require ui'; +'require rpc'; +'require uci'; +'require form'; +'require network'; +'require firewall'; +'require tools.widgets as widgets'; + +const isReadonlyView = !L.hasViewPermission(); + +function count_changes(section_id) { + const changes = ui.changes.changes?.wireless; + if (!Array.isArray(changes)) return 0; + + return changes.reduce((count, [, id]) => count + (id === section_id), 0); +} + +function render_radio_badge(radioDev) { + return E('span', { 'class': 'ifacebadge' }, [ + E('img', { 'src': L.resource('icons/wifi%s.svg').format(radioDev.isUp() ? '' : '_disabled') }), + ' ', + radioDev.getName() + ]); +} + +function render_signal_badge(signalPercent, signalValue, noiseValue, wrap, mode) { + let icon = L.resource('icons/signal-075-100.svg'), title, value; + + switch(true) { + case(signalPercent < 0): icon = L.resource('icons/signal-none.svg'); break; + case(signalPercent == 0): icon = L.resource('icons/signal-000-000.svg'); break; + case(signalPercent < 25): icon = L.resource('icons/signal-000-025.svg'); break; + case(signalPercent < 50): icon = L.resource('icons/signal-025-050.svg'); break; + case(signalPercent < 75): icon = L.resource('icons/signal-050-075.svg'); break; + } + + if (signalValue) { + if (noiseValue) { + value = `${signalValue}/${noiseValue}\xa0${_('dBm')}`; + title = [ + `${_('Signal')}: ${signalValue} ${_('dBm')}`, + `${_('Noise')}: ${noiseValue} ${_('dBm')}`, + `${_('SNR')}: ${signalValue - noiseValue} ${_('dBm')}` + ].filter(Boolean).join(' / '); + } + else { + value = `${signalValue}\xa0${_('dBm')}`; + title = `${_('Signal')} ${signalValue} ${_('dBm')}`; + } + } + else if (signalPercent > -1) { + switch (mode) { + case 'ap': + title = _('No client associated'); + break; + + case 'sta': + case 'adhoc': + case 'mesh': + title = _('Not associated'); + break; + + default: + title = _('No RX signal'); + } + + if (noiseValue) { + value = `---/${noiseValue}\xa0${_('dBm')}`; + title = `${title} / ${_('Noise')}: ${noiseValue} ${_('dBm')}`; + } + else { + value = `---\xa0${_('dBm')}`; + } + } + else { + value = E('em', {}, E('small', {}, [ _('disabled') ])); + title = _('Interface is disabled'); + } + + return E('div', { + 'class': wrap ? 'center' : 'ifacebadge', + 'title': title, + 'data-signal': signalValue, + 'data-noise': noiseValue + }, [ + E('img', { 'src': icon }), + E('span', {}, [ + wrap ? E('br') : ' ', + value + ]) + ]); +} + +function render_network_badge(radioNet) { + return render_signal_badge( + radioNet.isUp() ? radioNet.getSignalPercent() : -1, + radioNet.getSignal(), radioNet.getNoise(), false, radioNet.getMode()); +} + +function render_radio_status(radioDev, wifiNets) { + const name = radioDev.getI18n().replace(/ Wireless Controller .+$/, ''); + const node = E('div', [ E('big', {}, E('strong', {}, name)), E('div') ]); + let channel, frequency, bitrate; + + wifiNets.forEach(wifiNet => { + channel = channel ?? wifiNet.getChannel(); + frequency = frequency ?? wifiNet.getFrequency(); + bitrate = bitrate ?? wifiNet.getBitRate(); + }); + + if (radioDev.isUp()) + L.itemlist(node.lastElementChild, [ + _('Channel'), `${channel || '?'} (${frequency || '?'} ${_('GHz')})`, + _('Bitrate'), `${bitrate || '?'} ${_('Mbit/s')}` + ], ' | '); + else + node.lastElementChild.appendChild(E('em', _('Device is not active'))); + + return node; +} + +function render_network_status(radioNet) { + const mode = radioNet.getActiveMode(); + const bssid = radioNet.getActiveBSSID(); + const channel = radioNet.getChannel(); + const disabled = (radioNet.get('disabled') == '1' || uci.get('wireless', radioNet.getWifiDeviceName(), 'disabled') == '1'); + const is_assoc = (bssid && bssid != '00:00:00:00:00:00' && channel && mode != 'Unknown' && !disabled); + const is_mesh = (radioNet.getMode() == 'mesh'); + const changecount = count_changes(radioNet.getName()); + let status_text = null; + + if (changecount) + status_text = E('a', { + href: '#', + click: L.bind(ui.changes.displayChanges, ui.changes) + }, _('Interface has %d pending changes').format(changecount)); + else if (!is_assoc) + status_text = E('em', disabled ? _('Wireless is disabled') : _('Wireless is not associated')); + + return L.itemlist(E('div'), [ + is_mesh ? _('Mesh ID') : _('SSID'), (is_mesh ? radioNet.getMeshID() : (IW_ESSID[radioNet.getIfname()] || radioNet.getActiveSSID())) ?? '?', + _('Mode'), mode, + _('BSSID'), (!changecount && is_assoc) ? bssid : null, + _('Encryption'), (!changecount && is_assoc) ? radioNet.getActiveEncryption() ?? _('None') : null, + '', status_text + ], [ ' | ', E('br') ]); +} + +function render_modal_status(node, radioNet) { + if (!radioNet) return; + + const mode = radioNet.getActiveMode(); + const noise = radioNet.getNoise(); + const bssid = radioNet.getActiveBSSID(); + const channel = radioNet.getChannel(); + const disabled = (radioNet.get('disabled') == '1'); + const is_assoc = (bssid && bssid != '00:00:00:00:00:00' && channel && mode != 'Unknown' && !disabled); + + if (node == null) + node = E('span', { 'class': 'ifacebadge large', 'data-network': radioNet.getName() }, [ E('small'), E('span') ]); + + dom.content(node.firstElementChild, render_signal_badge( + disabled ? -1 : radioNet.getSignalPercent(), + radioNet.getSignal(), noise, true, radioNet.getMode())); + + L.itemlist(node.lastElementChild, [ + _('Mode'), mode, + _('SSID'), (IW_ESSID[radioNet.getIfname()] || radioNet.getActiveSSID()) ?? '?', + _('BSSID'), is_assoc ? bssid : null, + _('Encryption'), is_assoc ? radioNet.getActiveEncryption() ?? _('None') : null, + _('Channel'), is_assoc ? `${radioNet.getChannel()} (${radioNet.getFrequency() ?? 0} ${_('GHz')})` : null, + _('Tx-Power'), is_assoc ? `${radioNet.getTXPower()} ${_('dBm')}` : null, + _('Signal'), is_assoc ? `${radioNet.getSignal()} ${_('dBm')}` : null, + _('Noise'), (is_assoc && noise != null) ? `${noise} ${_('dBm')}` : null, + _('Bitrate'), is_assoc ? `${radioNet.getBitRate() ?? 0} ${_('Mbit/s')}` : null, + _('Country'), is_assoc ? radioNet.getCountryCode() : null + ], [ ' | ', E('br'), E('br'), E('br'), E('br'), E('br'), ' | ', E('br'), ' | ' ]); + + if (!is_assoc) + dom.append(node.lastElementChild, E('em', disabled ? _('Wireless is disabled') : _('Wireless is not associated'))); + + return node; +} + +function format_wifirate(rate) { + let s = `${rate.rate / 1000}\xa0${_('Mbit/s')}, ${rate.mhz}\xa0${_('MHz')}`; + const ht = rate.ht; + const vht = rate.vht; + const nss = rate.nss; + const mcs = rate.mcs; + const sgi = rate.short_gi; + const he = rate.he; + const he_gi = rate.he_gi; + const he_dcm = rate.he_dcm; + const eht = rate?.eht ?? false; + const eht_gi = rate?.eht_gi ?? 0; + const eht_dcm = rate?.eht_dcm ?? 0; + + if (ht || vht) s += [ + vht && `, VHT-MCS\xa0${mcs}`, + nss && `, VHT-NSS\xa0${nss}`, + ht && `, MCS\xa0${mcs}`, + sgi && ', ' + _('Short GI').replace(/ /g, '\xa0') + ].filter(Boolean).join(''); + + if (he) s += [ + `, HE-MCS\xa0${mcs}`, + nss && `, HE-NSS\xa0${nss}`, + he_gi && `, HE-GI\xa0${he_gi}`, + he_dcm && `, HE-DCM\xa0${he_dcm}` + ].filter(Boolean).join(''); + + if (eht) s += [ + `, EHT-MCS\xa0${mcs}`, + nss && `, EHT-NSS\xa0${nss}`, + he_gi && `, EHT-GI\xa0${eht_gi}`, + he_dcm && `, EHT-DCM\xa0${eht_dcm}` + ].filter(Boolean).join(''); + + return s; +} + +function radio_restart(id, ev) { + const row = document.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(id)); + const dsc = row.querySelector('[data-name="_stat"] > div'); + const btn = row.querySelector('.cbi-section-actions button'); + + btn.blur(); + btn.classList.add('spinning'); + btn.disabled = true; + + dsc.setAttribute('restart', ''); + dom.content(dsc, E('em', _('Device is restarting…'))); +} + +function network_updown(id, map, ev) { + const radio = uci.get('wireless', id, 'device'); + const disabled = (uci.get('wireless', id, 'disabled') == '1') || + (uci.get('wireless', radio, 'disabled') == '1'); + + if (disabled) { + uci.unset('wireless', id, 'disabled'); + uci.unset('wireless', radio, 'disabled'); + } + else { + uci.set('wireless', id, 'disabled', '1'); + + let all_networks_disabled = true; + const wifi_ifaces = uci.sections('wireless', 'wifi-iface'); + + wifi_ifaces.forEach(wifi_iface => { + if (wifi_iface.device == radio && wifi_iface.disabled != '1') + all_networks_disabled = false; + }); + + if (all_networks_disabled) + uci.set('wireless', radio, 'disabled', '1'); + } + + return map.save().then(function() { + ui.changes.apply(); + }); +} + +function next_free_sid(offset) { + let sid = 'wifinet' + offset; + + while (uci.get('wireless', sid)) + sid = 'wifinet' + (++offset); + + return sid; +} + +function add_dependency_permutations(o, deps) { + let res = [{}]; + + Object.entries(deps).forEach(([key, list]) => { + if (!Array.isArray(list)) return; + + res = list.flatMap(value => res.map(item => ({ ...item, [key]: value }))); + }); + + res.forEach(dep => o.depends(dep)); +} + +// Define a class CBIWifiFrequencyValue that extends form.Value +var CBIWifiFrequencyValue = form.Value.extend({ + // Declare an RPC method to get the frequency list for a given device + callFrequencyList: rpc.declare({ + object: 'iwinfo', + method: 'freqlist', + params: [ 'device' ], + expect: { results: [] } + }), + + // Load method to fetch WiFi device details and frequency list + load: function(section_id) { + // v33: query the PHYSICAL ifname (netifd-reported, band-remapped) + // instead of the UCI section name - iwinfo resolves section names + // statically while ra*/rax*<->PHY binding is random per boot. + const freqdev = (this.wifiNetwork && this.wifiNetwork.getIfname()) || section_id; + + return Promise.all([ + network.getWifiDevice(section_id), + this.callFrequencyList(freqdev) + ]).then(L.bind(function(data) { + this.channels = { + '2g': L.hasSystemFeature('hostapd', 'acs') ? [ 'auto', 'auto', { available: true } ] : [], + '5g': L.hasSystemFeature('hostapd', 'acs') ? [ 'auto', 'auto', { available: true } ] : [], + '6g': L.hasSystemFeature('hostapd', 'acs') ? [ 'auto', 'auto', { available: true } ] : [], + '60g': [] + }; + const wifidevs = data[0]; + const freqlist = data[1]; + + freqlist.forEach(freq => { + if (!freq.band) + return; + + const band = '%dg'.format(freq.band); + const available = (freq.restricted && freq.no_ir) ? false: true; + + this.channels[band].push( + freq.channel, + '%d (%d Mhz)'.format(freq.channel, freq.mhz), + { + available: available, + no_outdoor: freq.no_outdoor + } + + ); + }); + + const hwmodelist = L.toArray(wifidevs ? wifidevs.getHWModes() : null) + .reduce(function(o, v) { o[v] = true; return o; }, {}); + + // Define supported modes + this.modes = [ + '', 'Legacy', { available: hwmodelist.a || hwmodelist.b || hwmodelist.g }, + 'n', 'N', { available: hwmodelist.n }, + 'ac', 'AC', { available: L.hasSystemFeature('hostapd', '11ac') && hwmodelist.ac }, + 'ax', 'AX', { available: L.hasSystemFeature('hostapd', '11ax') && hwmodelist.ax }, + 'be', 'BE', { available: L.hasSystemFeature('hostapd', '11be') && hwmodelist.be } + ]; + + // Create a list of HT modes based on device capabilities + const htmodelist = L.toArray(wifidevs ? wifidevs.getHTModes() : null) + .reduce(function(o, v) { o[v] = true; return o; }, {}); + + this.htmodes = { + '': [ '', '-', { available: true } ], + 'n': [ + 'HT20', '20 MHz', { available: htmodelist.HT20 }, + 'HT40', '40 MHz', { available: htmodelist.HT40 } + ], + 'ac': [ + 'VHT20', '20 MHz', { available: htmodelist.VHT20 }, + 'VHT40', '40 MHz', { available: htmodelist.VHT40 }, + 'VHT80', '80 MHz', { available: htmodelist.VHT80 }, + 'VHT160', '160 MHz', { available: htmodelist.VHT160 } + ], + 'ax': [ + 'HE20', '20 MHz', { available: htmodelist.HE20 }, + 'HE40', '40 MHz', { available: htmodelist.HE40 }, + 'HE80', '80 MHz', { available: htmodelist.HE80 }, + 'HE160', '160 MHz', { available: htmodelist.HE160 } + ], + 'be': [ + 'EHT20', '20 MHz', { available: htmodelist.EHT20 }, + 'EHT40', '40 MHz', { available: htmodelist.EHT40 }, + 'EHT80', '80 MHz', { available: htmodelist.EHT80 }, + 'EHT160', '160 MHz', { available: htmodelist.EHT160 }, + 'EHT320', '320 MHz', { available: htmodelist.EHT320 } + ] + }; + + // Define available bands for widget selection based on channel availability + // AX and BE are available on 2/5/6G bands + this.bands = { + '': [ + '2g', '2.4 GHz', { available: this.channels['2g'].length > 3 }, + '5g', '5 GHz', { available: this.channels['5g'].length > 3 }, + '60g', '60 GHz', { available: this.channels['60g'].length > 0 } + ], + 'n': [ + '2g', '2.4 GHz', { available: this.channels['2g'].length > 3 }, + '5g', '5 GHz', { available: this.channels['5g'].length > 3 } + ], + 'ac': [ + '5g', '5 GHz', { available: true } + ], + 'ax': [ + '2g', '2.4 GHz', { available: this.channels['2g'].length > 3 }, + '5g', '5 GHz', { available: this.channels['5g'].length > 3 }, + '6g', '6 GHz', { available: this.channels['6g'].length > 3 } + ], + 'be': [ + '2g', '2.4 GHz', { available: this.channels['2g'].length > 3 }, + '5g', '5 GHz', { available: this.channels['5g'].length > 3 }, + '6g', '6 GHz', { available: this.channels['6g'].length > 3 } + ] + }; + }, this)); + }, + + // Set values in the select element + setValues: function(sel, vals) { + if (sel.vals) + sel.vals.selected = sel.selectedIndex; + + sel.options.length = 0; + + for (let i = 0; vals && i < vals.length; i += 3) + if (vals[i+2]?.available) + sel.add(E('option', { value: vals[i] }, [ vals[i+1] ])); + + if (Number.isInteger(vals?.selected)) sel.selectedIndex = vals.selected; + + sel.parentNode.style.display = (sel.options.length <= 1) ? 'none' : ''; + sel.vals = vals; + }, + + toggleWifiMode: function(elem) { + this.toggleWifiHTMode(elem); + this.toggleWifiBand(elem); + }, + + toggleWifiHTMode: function(elem) { + const mode = elem.querySelector('.mode'); + const bwdt = elem.querySelector('.htmode'); + + this.setValues(bwdt, this.htmodes[mode.value]); + }, + + toggleWifiBand: function(elem) { + const mode = elem.querySelector('.mode'); + const band = elem.querySelector('.band'); + + this.setValues(band, this.bands[mode.value]); + this.toggleWifiChannel(elem); + + this.map.checkDepends(); + }, + + checkWifiChannelRestriction: function(elem) { + const band = elem.querySelector('.band'); + const chan = elem.querySelector('.channel'); + const restricted_chan = elem.querySelector('.restricted_channel'); + const channels = this.channels[band.value]; + + if (chan.selectedIndex < 0) + return; + + const no_outdoor = channels[(chan.selectedIndex*3)+2].no_outdoor; + restricted_chan.style.display = no_outdoor ? '': 'none'; + }, + + toggleWifiChannel: function(elem) { + const band = elem.querySelector('.band'); + const chan = elem.querySelector('.channel'); + + this.setValues(chan, this.channels[band.value]); + + this.map.checkDepends(); + this.checkWifiChannelRestriction(elem); + }, + + setInitialValues: function(section_id, elem) { + const mode = elem.querySelector('.mode'); + const band = elem.querySelector('.band'); + const chan = elem.querySelector('.channel'); + const bwdt = elem.querySelector('.htmode'); + const htval = uci.get('wireless', section_id, 'htmode'); + const hwval = uci.get('wireless', section_id, 'hwmode'); + const chval = uci.get('wireless', section_id, 'channel'); + const bandval = uci.get('wireless', section_id, 'band'); + + this.setValues(mode, this.modes); + + // Determine mode based on htmode value + if (/EHT20|EHT40|EHT80|EHT160|EHT320/.test(htval)) + mode.value = 'be'; + else if (/HE20|HE40|HE80|HE160/.test(htval)) + mode.value = 'ax'; + else if (/VHT20|VHT40|VHT80|VHT160/.test(htval)) + mode.value = 'ac'; + else if (/HT20|HT40/.test(htval)) + mode.value = 'n'; + else + mode.value = ''; + + this.toggleWifiMode(elem); + + if (hwval != null) { + this.useBandOption = false; + + band.value = /a/.test(hwval) ? '5g': '2g'; + } + else { + this.useBandOption = true; + + band.value = bandval; + } + + this.toggleWifiBand(elem); + + bwdt.value = htval; + chan.value = chval ?? (chan.options[0] ? chan.options[0].value : 'auto'); + + this.checkWifiChannelRestriction(elem); + + return elem; + }, + + renderWidget: function(section_id, option_index, cfgvalue) { + const elem = E('div'); + + dom.content(elem, [ + E('div', { 'class' : 'restricted_channel', 'style': 'display:none'}, [ + E('div', {'class': 'cbi-button alert-message warning disabled'}, _('Indoor Only Channel Selected')) + ]), + E('label', { 'style': 'float:left; margin-right:3px' }, [ + _('Mode'), E('br'), + E('select', { + 'class': 'mode', + 'style': 'width:auto', + 'change': L.bind(this.toggleWifiMode, this, elem), + 'disabled': (this.disabled != null) ? this.disabled : this.map.readonly + }) + ]), + E('label', { 'style': 'float:left; margin-right:3px' }, [ + _('Band'), E('br'), + E('select', { + 'class': 'band', + 'style': 'width:auto', + 'change': L.bind(this.toggleWifiBand, this, elem), + 'disabled': (this.disabled != null) ? this.disabled : this.map.readonly + }) + ]), + E('label', { 'style': 'float:left; margin-right:3px' }, [ + _('Channel'), E('br'), + E('select', { + 'class': 'channel', + 'style': 'width:auto', + 'change': L.bind(this.toggleWifiChannel, this, elem), + 'disabled': (this.disabled != null) ? this.disabled : this.map.readonly + }) + ]), + E('label', { 'style': 'float:left; margin-right:3px' }, [ + _('Width'), E('br'), + E('select', { + 'class': 'htmode', + 'style': 'width:auto', + 'change': L.bind(this.map.checkDepends, this.map), + 'disabled': (this.disabled != null) ? this.disabled : this.map.readonly + }) + ]), + E('br', { 'style': 'clear:left' }) + ]); + + return this.setInitialValues(section_id, elem); + }, + + cfgvalue: function(section_id) { + return [ + uci.get('wireless', section_id, 'htmode'), + uci.get('wireless', section_id, 'hwmode') || uci.get('wireless', section_id, 'band'), + uci.get('wireless', section_id, 'channel') + ]; + }, + + formvalue: function(section_id) { + const node = this.map.findElement('data-field', this.cbid(section_id)); + + return [ + node.querySelector('.htmode').value, + node.querySelector('.band').value, + node.querySelector('.channel').value + ]; + }, + + write: function(section_id, value) { + uci.set('wireless', section_id, 'htmode', value[0] || null); + + this.useBandOption ? + uci.set('wireless', section_id, 'band', value[1]) : + uci.set('wireless', section_id, 'hwmode', (value[1] == '2g') ? '11g' : '11a'); + + uci.set('wireless', section_id, 'channel', value[2]); + } +}); + +var CBIWifiTxPowerValue = form.ListValue.extend({ + callTxPowerList: rpc.declare({ + object: 'iwinfo', + method: 'txpowerlist', + params: [ 'device' ], + expect: { results: [] } + }), + + load: function(section_id) { + // v33: physical ifname, same rationale as CBIWifiFrequencyValue. + const pwrdev = (this.wifiNetwork && this.wifiNetwork.getIfname()) || section_id; + + return this.callTxPowerList(pwrdev).then(L.bind(function(pwrlist) { + this.powerval = this.wifiNetwork ? this.wifiNetwork.getTXPower() : null; + this.poweroff = this.wifiNetwork ? this.wifiNetwork.getTXPowerOffset() : null; + + this.value('', _('driver default')); + + for (let p of pwrlist) + this.value(p.dbm, `${p.dbm} dBm (${p.mw} mW)`); + + return form.ListValue.prototype.load.apply(this, [section_id]); + }, this)); + }, + + renderWidget: function(section_id, option_index, cfgvalue) { + const widget = form.ListValue.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]); + + widget.firstElementChild.style.width = 'auto'; + + dom.append(widget, E('span', [ + ' - ', _('Current power'), ': ', + E('span', [ this.powerval != null ? `${this.powerval} dBm` : E('em', _('unknown')) ]), + this.poweroff ? ` + ${this.poweroff} dB offset = ${this.powerval != null ? this.powerval + this.poweroff : '?'} dBm` : '' + ])); + + return widget; + } +}); + +var CBIWifiCountryValue = form.Value.extend({ + callCountryList: rpc.declare({ + object: 'iwinfo', + method: 'countrylist', + params: [ 'device' ], + expect: { results: [] } + }), + + load: function(section_id) { + return this.callCountryList(section_id).then(L.bind(function(countrylist) { + if (Array.isArray(countrylist) && countrylist.length > 0) { + this.value('', _('driver default')); + + for (let c of countrylist) + this.value(c.iso3166, `${c.iso3166} - ${c.country}`); + } + + return form.Value.prototype.load.apply(this, [section_id]); + }, this)); + }, + + validate: function(section_id, formvalue) { + if (formvalue != null && formvalue != '' && !/^[A-Z0-9][A-Z0-9]$/.test(formvalue)) + return _('Use ISO/IEC 3166 alpha2 country codes.'); + + return true; + }, + + renderWidget: function(section_id, option_index, cfgvalue) { + const typeClass = (this.keylist && this.keylist.length) ? form.ListValue : form.Value; + return typeClass.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]); + } +}); + +return view.extend({ + poll_status: function(map, data) { + const rows = map.querySelectorAll('.cbi-section-table-row[data-sid]'); + + rows.forEach(row => { + const section_id = row.getAttribute('data-sid'); + const radioDev = data[1].filter(function(d) { return d.getName() == section_id; })[0]; + const radioNet = data[2].filter(function(n) { return n.getName() == section_id; })[0]; + const badge = row.querySelector('[data-name="_badge"] > div'); + const stat = row.querySelector('[data-name="_stat"]'); + const btns = row.querySelectorAll('.cbi-section-actions button'); + const busy = btns[0].classList.contains('spinning') || btns[1].classList.contains('spinning') || btns[2].classList.contains('spinning'); + + if (radioDev) { + dom.content(badge, render_radio_badge(radioDev)); + dom.content(stat, render_radio_status(radioDev, data[2].filter(function(n) { return n.getWifiDeviceName() == radioDev.getName(); }))); + } + else { + dom.content(badge, render_network_badge(radioNet)); + dom.content(stat, render_network_status(radioNet)); + } + + if (stat.hasAttribute('restart')) + dom.content(stat, E('em', _('Device is restarting…'))); + + btns[0].disabled = isReadonlyView || busy; + btns[1].disabled = (isReadonlyView && radioDev) || busy; + btns[2].disabled = isReadonlyView || busy; + }); + + const table = document.querySelector('#wifi_assoclist_table'); + const hosts = data[0]; + let trows = []; + const radios = data[3]; + const zones = data[4]; + + radios.forEach(zone => { + const bss = zone; + const name = hosts.getHostnameByMACAddr(bss.mac); + const ipv4 = hosts.getIPAddrByMACAddr(bss.mac); + const ipv6 = hosts.getIP6AddrByMACAddr(bss.mac); + + let hint; + + if (name && ipv4 && ipv6) + hint = `${'%h'.format(name)} (${ipv4}, ${ipv6})`; + else if (name && (ipv4 ?? ipv6)) + hint = `${'%h'.format(name)} (${ipv4 || ipv6})`; + else + hint = name || ipv4 || ipv6 || '?'; + + let row = [ + E('span', { + 'class': 'ifacebadge', + 'data-ifname': bss.network.getIfname(), + 'data-ssid': bss.network.getSSID() + }, [ + E('img', { + 'src': L.resource('icons/wifi%s.svg').format(bss.network.isUp() ? '' : '_disabled'), + 'title': bss.radio.getI18n() + }), + E('span', [ + ` ${bss.network.getShortName()} `, + E('small', `(${bss.network.getIfname()})`) + ]) + ]), + bss.mac, + hint, + render_signal_badge(Math.min((bss.signal + 110) / 70 * 100, 100), bss.signal, bss.noise), + E('span', {}, [ + E('span', format_wifirate(bss.rx)), + E('br'), + E('span', format_wifirate(bss.tx)) + ]) + ]; + + if (bss.vlan) { + const desc = bss.vlan.getI18n(); + const vlan_network = bss.vlan.getNetwork(); + let vlan_zone; + + if (vlan_network && zones) + for (let zone of zones) + if (zone.getNetworks().includes(vlan_network)) + vlan_zone = zone; + + row[0].insertBefore( + E('div', { + 'class' : 'zonebadge', + 'title' : desc, + 'style' : firewall.getZoneColorStyle(vlan_zone) + }, [ desc ]), row[0].firstChild); + } + + if (bss.network.isClientDisconnectSupported()) { + if (table.firstElementChild.childNodes.length < 6) + table.firstElementChild.appendChild(E('th', { 'class': 'th cbi-section-actions'})); + + row.push(E('button', { + 'class': 'cbi-button cbi-button-remove', + 'click': L.bind(function(net, mac, ev) { + dom.parent(ev.currentTarget, '.tr').style.opacity = 0.5; + ev.currentTarget.classList.add('spinning'); + ev.currentTarget.disabled = true; + ev.currentTarget.blur(); + + net.disconnectClient(mac, true, 5, 60000); + }, this, bss.network, bss.mac), + 'disabled': isReadonlyView || null + }, [ _('Disconnect') ])); + } + else { + row.push('-'); + } + + trows.push(row); + }); + + cbi_update_table(table, trows, E('em', _('No information available'))); + + const status = document.querySelector('.cbi-modal [data-name="_wifistat_modal"] .ifacebadge.large'); + + if (status) + render_modal_status(status, data[2].filter(function(n) { return n.getName() == status.getAttribute('data-network'); })[0]); + + return network.flushCache(); + }, + + load: function() { + return Promise.all([ + uci.changes(), + uci.load('wireless'), + uci.load('system'), + firewall.getZones(), + ]); + }, + + checkAnonymousSections: function() { + return uci.sections('wireless', 'wifi-iface').some(iface => iface['.anonymous']); + }, + + callUciRename: rpc.declare({ + object: 'uci', + method: 'rename', + params: [ 'config', 'section', 'name' ] + }), + + render: function(data) { + if (this.checkAnonymousSections()) + return this.renderMigration(); + else + return this.renderOverview(data[3]); + }, + + handleMigration: function(ev) { + const wifiIfaces = uci.sections('wireless', 'wifi-iface'); + let id_offset = 0; + const tasks = []; + + wifiIfaces.forEach((iface) => { + if (iface['.anonymous']) { + const new_name = next_free_sid(id_offset); + tasks.push(this.callUciRename('wireless', iface['.name'], new_name)); + id_offset = parseInt(new_name.substring(7), 10) + 1; + } + }); + + return Promise.all(tasks) + .then(L.bind(ui.changes.init, ui.changes)) + .then(L.bind(ui.changes.apply, ui.changes)); + }, + + renderMigration: function() { + ui.showModal(_('Wireless configuration migration'), [ + E('p', _('The existing wireless configuration needs to be changed for LuCI to function properly.')), + E('p', _('Upon pressing "Continue", anonymous "wifi-iface" sections will be assigned with a name in the form wifinet# and the network will be restarted to apply the updated configuration.')), + E('div', { 'class': 'right' }, + E('button', { + 'class': 'btn cbi-button-action important', + 'click': ui.createHandlerFn(this, 'handleMigration') + }, _('Continue'))) + ]); + }, + + renderOverview: function(zones) { + let m, s, o; + + m = new form.Map('wireless'); + m.chain('network'); + m.chain('firewall'); + + s = m.section(form.GridSection, 'wifi-device', _('Wireless Overview')); + s.anonymous = true; + s.addremove = false; + + s.load = function() { + return network.getWifiDevices().then(L.bind(function(radios) { + this.radios = radios.sort(function(a, b) { + return a.getName() > b.getName(); + }); + + const tasks = []; + + radios.forEach(radio => { + tasks.push(radio.getWifiNetworks()); + }); + + return Promise.all(tasks); + }, this)).then(L.bind(function(data) { + this.wifis = []; + + data.forEach(d => { + this.wifis.push.apply(this.wifis, d); + }); + }, this)); + }; + + s.cfgsections = function() { + const rv = []; + + this.radios.forEach(radio => { + rv.push(radio.getName()); + + this.wifis.forEach(wifi => { + if (wifi.getWifiDeviceName() == radio.getName()) + rv.push(wifi.getName()); + }); + }); + + return rv; + }; + + s.modaltitle = function(section_id) { + const radioNet = this.wifis.filter(function(w) { return w.getName() == section_id; })[0]; + return radioNet ? radioNet.getI18n() : _('Edit wireless network'); + }; + + s.lookupRadioOrNetwork = function(section_id) { + const radioDev = this.radios.filter(function(r) { return r.getName() == section_id; })[0]; + if (radioDev) + return radioDev; + + const radioNet = this.wifis.filter(function(w) { return w.getName() == section_id; })[0]; + if (radioNet) + return radioNet; + + return null; + }; + + s.renderRowActions = function(section_id) { + const inst = this.lookupRadioOrNetwork(section_id); + let btns; + + if (inst.getWifiNetworks) { + btns = [ + E('button', { + 'class': 'cbi-button cbi-button-neutral', + 'title': _('Restart radio interface'), + 'click': ui.createHandlerFn(this, radio_restart, section_id) + }, _('Restart')), + E('button', { + 'class': 'cbi-button cbi-button-action important', + 'title': _('Find and join network'), + 'click': ui.createHandlerFn(this, 'handleScan', inst) + }, _('Scan')), + E('button', { + 'class': 'cbi-button cbi-button-add', + 'title': _('Provide new network'), + 'click': ui.createHandlerFn(this, 'handleAdd', inst) + }, _('Add')) + ]; + } + else { + const isDisabled = (inst.get('disabled') == '1' || + uci.get('wireless', inst.getWifiDeviceName(), 'disabled') == '1'); + + btns = [ + E('button', { + 'class': 'cbi-button cbi-button-neutral enable-disable', + 'title': isDisabled ? _('Enable this network') : _('Disable this network'), + 'click': ui.createHandlerFn(this, network_updown, section_id, this.map) + }, isDisabled ? _('Enable') : _('Disable')), + E('button', { + 'class': 'cbi-button cbi-button-action important', + 'title': _('Edit this network'), + 'click': ui.createHandlerFn(this, 'renderMoreOptionsModal', section_id) + }, _('Edit')), + E('button', { + 'class': 'cbi-button cbi-button-negative remove', + 'title': _('Delete this network'), + 'click': ui.createHandlerFn(this, 'handleRemove', section_id) + }, _('Remove')) + ]; + } + + return E('td', { 'class': 'td middle cbi-section-actions' }, E('div', btns)); + }; + + s.addModalOptions = function(s) { + return network.getWifiNetwork(s.section).then(function(radioNet) { + const hwtype = uci.get('wireless', radioNet.getWifiDeviceName(), 'type'); + const have_mesh = L.hasSystemFeature('hostapd', 'mesh') || L.hasSystemFeature('wpasupplicant', 'mesh'); + let o, ss; + + o = s.option(form.SectionValue, '_device', form.NamedSection, radioNet.getWifiDeviceName(), 'wifi-device', _('Device Configuration')); + o.modalonly = true; + + ss = o.subsection; + ss.tab('general', _('General Setup')); + ss.tab('advanced', _('Advanced Settings')); + + const isDisabled = (radioNet.get('disabled') == '1' || + uci.get('wireless', radioNet.getWifiDeviceName(), 'disabled') == 1); + + o = ss.taboption('general', form.DummyValue, '_wifistat_modal', _('Status')); + o.cfgvalue = L.bind(function(radioNet) { + return render_modal_status(null, radioNet); + }, this, radioNet); + o.write = function() {}; + + o = ss.taboption('general', form.Button, '_toggle', isDisabled ? _('Wireless network is disabled') : _('Wireless network is enabled')); + o.inputstyle = isDisabled ? 'apply' : 'reset'; + o.inputtitle = isDisabled ? _('Enable') : _('Disable'); + o.onclick = ui.createHandlerFn(s, network_updown, s.section, s.map); + + o = ss.taboption('general', CBIWifiFrequencyValue, '_freq', '
' + _('Operating frequency'), _('Some channels may be restricted to Indoor Only use by your Regulatory Domain. Make sure to follow this advice if a channel is reported as such.')); + o.ucisection = s.section; + o.wifiNetwork = radioNet; + + if (hwtype == 'mac80211') { + o = ss.taboption('general', form.Flag, 'legacy_rates', _('Allow legacy 802.11b rates'), _('Legacy or badly behaving devices may require legacy 802.11b rates to interoperate. Airtime efficiency may be significantly reduced where these are used. It is recommended to not allow 802.11b rates where possible.')); + o.depends({'_freq': '2g', '!contains': true}); + + o = ss.taboption('general', CBIWifiTxPowerValue, 'txpower', _('Maximum transmit power'), _('Specifies the maximum transmit power the wireless radio may use. Depending on regulatory requirements and wireless usage, the actual transmit power may be reduced by the driver.')); + o.wifiNetwork = radioNet; + + o = ss.taboption('general', CBIWifiCountryValue, 'country', _('Country Code')); + o.wifiNetwork = radioNet; + + o = ss.taboption('advanced', form.ListValue, 'cell_density', _('Coverage cell density'), _('Configures data rates based on the coverage cell density. Normal configures basic rates to 6, 12, 24 Mbps if legacy 802.11b rates are not used else to 5.5, 11 Mbps. High configures basic rates to 12, 24 Mbps if legacy 802.11b rates are not used else to the 11 Mbps rate. Very High configures 24 Mbps as the basic rate. Supported rates lower than the minimum basic rate are not offered.')); + o.value('0', _('Disabled')); + o.value('1', _('Normal')); + o.value('2', _('High')); + o.value('3', _('Very High')); + + o = ss.taboption('advanced', form.Flag, 'mu_beamformer', _('MU-MIMO')); + o.default = o.disabled; + + o = ss.taboption('advanced', form.Value, 'distance', _('Distance Optimization'), _('Distance to farthest network member in meters. Set only for distances above one kilometer; otherwise it is harmful.')); + o.datatype = 'or(range(0,114750),"auto")'; + o.placeholder = 'auto'; + + o = ss.taboption('advanced', form.Value, 'frag', _('Fragmentation Threshold')); + o.datatype = 'min(256)'; + o.placeholder = _('off'); + + o = ss.taboption('advanced', form.Value, 'rts', _('RTS/CTS Threshold')); + o.datatype = 'uinteger'; + o.placeholder = _('off'); + + o = ss.taboption('advanced', form.Flag, 'noscan', _('Force 40MHz mode'), _('Always use 40MHz channels even if the secondary channel overlaps. Using this option does not comply with IEEE 802.11n-2009!')); + o.rmempty = true; + + o = ss.taboption('advanced', form.Flag, 'vendor_vht', _('Enable 256-QAM'), _('802.11n 2.4Ghz Only')); + o.default = o.disabled; + + o = ss.taboption('advanced', form.Value, 'beacon_int', _('Beacon Interval')); + o.datatype = 'range(15,65535)'; + o.placeholder = 100; + o.rmempty = true; + + o = ss.taboption('advanced', form.Flag, 'rxldpc', _('Rx LDPC'), _('Low-Density Parity-Check')); + o.default = '1'; + + o = ss.taboption('advanced', form.Flag, 'ldpc', _('Tx LDPC')); + o.depends({'rxldpc': '1'}); + o.default = '1'; + } + + + o = s.option(form.SectionValue, '_device', form.NamedSection, radioNet.getName(), 'wifi-iface', _('Interface Configuration')); + o.modalonly = true; + + ss = o.subsection; + ss.tab('general', _('General Setup')); + ss.tab('encryption', _('Wireless Security')); + ss.tab('macfilter', _('MAC-Filter')); + ss.tab('advanced', _('Advanced Settings')); + ss.tab('roaming', _('WLAN roaming'), _('Settings for assisting wireless clients in roaming between multiple APs: 802.11r, 802.11k and 802.11v')); + + o = ss.taboption('general', form.ListValue, 'mode', _('Mode')); + o.value('ap', _('Access Point')); + o.value('sta', _('Client')); + o.value('adhoc', _('Ad-Hoc')); + + o = ss.taboption('general', form.Value, 'mesh_id', _('Mesh Id')); + o.depends('mode', 'mesh'); + + o = ss.taboption('advanced', form.Flag, 'mesh_fwding', _('Forward mesh peer traffic')); + o.rmempty = false; + o.default = '1'; + o.depends('mode', 'mesh'); + + o = ss.taboption('advanced', form.Value, 'mesh_rssi_threshold', _('RSSI threshold for joining mesh'), _('0 = not using RSSI threshold, 1 = do not change driver default') + ' ' + + _('Units: dBm. Where -255 is weakest, and -10 is strong.')); + o.rmempty = false; + o.default = '0'; + o.datatype = 'range(-255,1)'; + o.depends('mode', 'mesh'); + + o = ss.taboption('general', form.Value, 'ssid', _('ESSID')); + o.datatype = 'maxlength(32)'; + o.depends('mode', 'ap'); + o.depends('mode', 'sta'); + o.depends('mode', 'adhoc'); + o.depends('mode', 'ahdemo'); + o.depends('mode', 'monitor'); + o.depends('mode', 'ap-wds'); + o.depends('mode', 'sta-wds'); + o.depends('mode', 'wds'); + + o = ss.taboption('general', form.Value, 'bssid', _('BSSID')); + o.datatype = 'macaddr'; + + o = ss.taboption('general', widgets.NetworkSelect, 'network', _('Network'), _('Choose the network(s) you want to attach to this wireless interface or fill out the custom field to define a new network.')); + o.rmempty = true; + o.multiple = true; + o.novirtual = true; + o.write = function(section_id, value) { + return network.getDevice(section_id).then(L.bind(function(dev) { + const old_networks = dev.getNetworks().reduce(function(o, v) { o[v.getName()] = v; return o; }, {}); + const new_networks = {}; + const values = L.toArray(value); + const tasks = []; + + values.forEach(value => { + + new_networks[value] = true; + + if (old_networks[value]) + return; + + tasks.push(network.getNetwork(value).then(L.bind(function(name, net) { + return net || network.addNetwork(name, { proto: 'none' }); + }, this, value)).then(L.bind(function(dev, net) { + if (net) { + if (!net.isEmpty()) { + let target_dev = net.getDevice(); + + /* Resolve parent interface of vlan */ + while (target_dev && target_dev.getType() == 'vlan') + target_dev = target_dev.getParent(); + + if (!target_dev || target_dev.getType() != 'bridge') + net.set('type', 'bridge'); + } + + net.addDevice(dev); + } + }, this, dev))); + }); + + for (let name in old_networks) + if (!new_networks[name]) + tasks.push(network.getNetwork(name).then(L.bind(function(dev, net) { + if (net) + net.deleteDevice(dev); + }, this, dev))); + + return Promise.all(tasks); + }, this)); + }; + + let encr; + if (hwtype == 'mac80211') { + const mode = ss.children.find(obj => obj.option === 'mode'); + const bssid = ss.children.find(obj => obj.option === 'bssid'); + + if (have_mesh) mode.value('mesh', '802.11s'); + mode.value('ahdemo', _('Pseudo Ad-Hoc (ahdemo)')); + mode.value('monitor', _('Monitor')); + + bssid.depends('mode', 'adhoc'); + bssid.depends('mode', 'sta'); + bssid.depends('mode', 'sta-wds'); + + o = ss.taboption('macfilter', form.ListValue, 'macfilter', _('MAC Address Filter')); + o.depends('mode', 'ap'); + o.depends('mode', 'ap-wds'); + o.value('', _('disable')); + o.value('allow', _('Allow listed only')); + o.value('deny', _('Allow all except listed')); + + o = ss.taboption('macfilter', form.DynamicList, 'maclist', _('MAC-List')); + o.datatype = 'macaddr'; + o.retain = true; + o.depends('macfilter', 'allow'); + o.depends('macfilter', 'deny'); + o.load = function(section_id) { + return network.getHostHints().then(L.bind(function(hints) { + hints.getMACHints().map(L.bind(function(hint) { + this.value(hint[0], hint[1] ? '%s (%s)'.format(hint[0], hint[1]) : hint[0]); + }, this)); + + return form.DynamicList.prototype.load.apply(this, [section_id]); + }, this)); + }; + + mode.value('ap-wds', '%s (%s)'.format(_('Access Point'), _('WDS'))); + mode.value('sta-wds', '%s (%s)'.format(_('Client'), _('WDS'))); + + mode.write = function(section_id, value) { + switch (value) { + case 'ap-wds': + uci.set('wireless', section_id, 'mode', 'ap'); + uci.set('wireless', section_id, 'wds', '1'); + break; + + case 'sta-wds': + uci.set('wireless', section_id, 'mode', 'sta'); + uci.set('wireless', section_id, 'wds', '1'); + break; + + default: + uci.set('wireless', section_id, 'mode', value); + uci.unset('wireless', section_id, 'wds'); + break; + } + }; + + mode.cfgvalue = function(section_id) { + const mode = uci.get('wireless', section_id, 'mode'); + const wds = uci.get('wireless', section_id, 'wds'); + + if (mode == 'ap' && wds) + return 'ap-wds'; + else if (mode == 'sta' && wds) + return 'sta-wds'; + + return mode; + }; + + o = ss.taboption('general', form.Flag, 'hidden', _('Hide ESSID'), _('Where the ESSID is hidden, clients may fail to roam and airtime efficiency may be significantly reduced.')); + o.depends('mode', 'ap'); + o.depends('mode', 'ap-wds'); + + o = ss.taboption('general', form.Flag, 'wmm', _('WMM Mode'), _('Where Wi-Fi Multimedia (WMM) Mode QoS is disabled, clients may be limited to 802.11a/802.11g rates.')); + o.depends('mode', 'ap'); + o.depends('mode', 'ap-wds'); + o.default = o.enabled; + + /* https://w1.fi/cgit/hostap/commit/?id=34f7c699a6bcb5c45f82ceb6743354ad79296078 */ + /* multicast_to_unicast https://github.com/openwrt/openwrt/commit/7babb978ad9d7fc29acb1ff86afb1eb343af303a */ + o = ss.taboption('advanced', form.Flag, 'multicast_to_unicast_all', _('Multi To Unicast'), _('ARP, IPv4 and IPv6 (even 802.1Q) with multicast destination MACs are unicast to the STA MAC address. Note: This is not Directed Multicast Service (DMS) in 802.11v. Note: might break receiver STA multicast expectations.')); + o.rmempty = true; + + o = ss.taboption('advanced', form.Flag, 'isolate', _('Isolate Clients'), _('Prevents client-to-client communication')); + o.depends('mode', 'ap'); + o.depends('mode', 'ap-wds'); + + o = ss.taboption('advanced', form.Value, 'ifname', _('Interface name'), _('Override default interface name')); + o.optional = true; + o.datatype = 'netdevname'; + o.placeholder = radioNet.getIfname(); + if (/^radio\d+\.network/.test(o.placeholder)) + o.placeholder = ''; + + const macaddr = uci.get('wireless', radioNet.getName(), 'macaddr'); + o = ss.taboption('advanced', form.Value, 'macaddr', _('MAC address'), _('Override default MAC address - the range of usable addresses might be limited by the driver')); + o.value('', _('driver default (%s)').format(!macaddr ? radioNet.getActiveBSSID() : _('no override'))); + o.value('random', _('randomly generated')); + o.datatype = "or('random',macaddr)"; + + o = ss.taboption('advanced', form.Flag, 'short_preamble', _('Short Preamble')); + o.default = o.enabled; + + o = ss.taboption('advanced', form.Value, 'dtim_period', _('DTIM Interval'), _('Delivery Traffic Indication Message Interval')); + o.optional = true; + o.placeholder = 2; + o.datatype = 'range(1,255)'; + + o = ss.taboption('advanced', form.Value, 'wpa_group_rekey', _('Time interval for rekeying GTK'), _('sec')); + o.optional = true; + o.placeholder = 600; + o.datatype = 'uinteger'; + + o = ss.taboption('advanced', form.Flag , 'skip_inactivity_poll', _('Disable Inactivity Polling')); + o.optional = true; + o.datatype = 'uinteger'; + + o = ss.taboption('advanced', form.Value, 'max_inactivity', _('Station inactivity limit'), _('802.11v: BSS Max Idle. Units: seconds.')); + o.optional = true; + o.placeholder = 300; + o.datatype = 'uinteger'; + + o = ss.taboption('advanced', form.Value, 'max_listen_interval', _('Maximum allowed Listen Interval')); + o.optional = true; + o.placeholder = 65535; + o.datatype = 'uinteger'; + + o = ss.taboption('advanced', form.Flag, 'disassoc_low_ack', _('Disassociate On Low Acknowledgement'), _('Allow AP mode to disconnect STAs based on low ACK condition')); + o.default = o.enabled; + } + + + encr = o = ss.taboption('encryption', form.ListValue, 'encryption', _('Encryption')); + o.depends('mode', 'ap'); + o.depends('mode', 'sta'); + o.depends('mode', 'adhoc'); + o.depends('mode', 'ahdemo'); + o.depends('mode', 'ap-wds'); + o.depends('mode', 'sta-wds'); + o.depends('mode', 'mesh'); + + o.cfgvalue = function(section_id) { + const v = String(uci.get('wireless', section_id, 'encryption')); + if (v == 'wep') + return 'wep-open'; + else if (v.match(/\+/)) + return v.replace(/\+.+$/, ''); + return v; + }; + + o.write = function(section_id, value) { + let e = this.section.children.filter(function(o) { return o.option == 'encryption'; })[0].formvalue(section_id); + const co = this.section.children.filter(function(o) { return o.option == 'cipher'; })[0]; + const c = co.formvalue(section_id); + + if (value == 'wpa' || value == 'wpa2' || value == 'wpa3' || value == 'wpa3-mixed' || value == 'wpa3-192') + uci.unset('wireless', section_id, 'key'); + + if (co.isActive(section_id) && e && (c == 'tkip' || c == 'ccmp' || c == 'ccmp256' || c == 'gcmp' || c == 'gcmp256' || c == 'tkip+ccmp')) + e += '+' + c; + + uci.set('wireless', section_id, 'encryption', e); + }; + + o = ss.taboption('encryption', form.ListValue, 'cipher', _('Cipher')); + o.depends('encryption', 'wpa'); + o.depends('encryption', 'wpa2'); + o.depends('encryption', 'wpa3'); + o.depends('encryption', 'wpa3-mixed'); + o.depends('encryption', 'wpa3-192'); + o.depends('encryption', 'psk'); + o.depends('encryption', 'psk2'); + o.depends('encryption', 'wpa-mixed'); + o.depends('encryption', 'psk-mixed'); + o.value('auto', _('auto')); + o.value('ccmp', _('Force CCMP (AES)')); + o.value('ccmp256', _('Force CCMP-256 (AES)')); + o.value('gcmp', _('Force GCMP (AES)')); + o.value('gcmp256', _('Force GCMP-256 (AES)')); + o.value('tkip', _('Force TKIP')); + o.value('tkip+ccmp', _('Force TKIP and CCMP (AES)')); + o.write = ss.children.filter(function(o) { return o.option == 'encryption'; })[0].write; + + o.cfgvalue = function(section_id) { + let v = String(uci.get('wireless', section_id, 'encryption')); + if (v.match(/\+/)) { + v = v.replace(/^[^+]+\+/, ''); + if (v == 'aes') + v = 'ccmp'; + else if (v == 'tkip+aes' || v == 'aes+tkip' || v == 'ccmp+tkip') + v = 'tkip+ccmp'; + } + return v; + }; + + + const crypto_modes = []; + + if (hwtype == 'mac80211') { + const has_supplicant = L.hasSystemFeature('wpasupplicant'); + const has_hostapd = L.hasSystemFeature('hostapd'); + + // Probe EAP support + const has_ap_eap = L.hasSystemFeature('hostapd', 'eap'); + const has_sta_eap = L.hasSystemFeature('wpasupplicant', 'eap'); + + // Probe SAE support + const has_ap_sae = L.hasSystemFeature('hostapd', 'sae'); + const has_sta_sae = L.hasSystemFeature('wpasupplicant', 'sae'); + + // Probe OWE support + const has_ap_owe = L.hasSystemFeature('hostapd', 'owe'); + const has_sta_owe = L.hasSystemFeature('wpasupplicant', 'owe'); + + // Probe Suite-B support + const has_ap_eap192 = L.hasSystemFeature('hostapd', 'suiteb192'); + const has_sta_eap192 = L.hasSystemFeature('wpasupplicant', 'suiteb192'); + + // Probe WEP support + const has_ap_wep = L.hasSystemFeature('hostapd', 'wep'); + const has_sta_wep = L.hasSystemFeature('wpasupplicant', 'wep'); + + if (has_hostapd || has_supplicant) { + crypto_modes.push(['psk2', 'WPA2-PSK', 35]); + crypto_modes.push(['psk-mixed', 'WPA-PSK/WPA2-PSK Mixed Mode', 22]); + crypto_modes.push(['psk', 'WPA-PSK', 12]); + } + else { + encr.description = _('WPA-Encryption requires wpa_supplicant (for client mode) or hostapd (for AP and ad-hoc mode) to be installed.'); + } + + if (has_ap_sae || has_sta_sae) { + crypto_modes.push(['sae', 'WPA3-SAE', 31]); + crypto_modes.push(['sae-mixed', 'WPA2-PSK/WPA3-SAE Mixed Mode', 30]); + } + + if (has_ap_wep || has_sta_wep) { + crypto_modes.push(['wep-open', _('WEP Open System'), 11]); + crypto_modes.push(['wep-shared', _('WEP Shared Key'), 10]); + } + + if (has_ap_eap || has_sta_eap) { + if (has_ap_eap192 || has_sta_eap192) { + crypto_modes.push(['wpa3', 'WPA3-EAP', 33]); + crypto_modes.push(['wpa3-mixed', 'WPA2-EAP/WPA3-EAP Mixed Mode', 32]); + crypto_modes.push(['wpa3-192', 'WPA3-EAP 192-bit Mode', 36]); + } + + crypto_modes.push(['wpa2', 'WPA2-EAP', 34]); + crypto_modes.push(['wpa', 'WPA-EAP', 20]); + } + + if (has_ap_owe || has_sta_owe) { + crypto_modes.push(['owe', 'OWE', 1]); + } + + encr.crypto_support = { + 'ap': { + 'wep-open': has_ap_wep || _('Requires hostapd with WEP support'), + 'wep-shared': has_ap_wep || _('Requires hostapd with WEP support'), + 'psk': has_hostapd || _('Requires hostapd'), + 'psk2': has_hostapd || _('Requires hostapd'), + 'psk-mixed': has_hostapd || _('Requires hostapd'), + 'sae': has_ap_sae || _('Requires hostapd with SAE support'), + 'sae-mixed': has_ap_sae || _('Requires hostapd with SAE support'), + 'wpa': has_ap_eap || _('Requires hostapd with EAP support'), + 'wpa2': has_ap_eap || _('Requires hostapd with EAP support'), + 'wpa3': has_ap_eap192 || _('Requires hostapd with EAP Suite-B support'), + 'wpa3-mixed': has_ap_eap192 || _('Requires hostapd with EAP Suite-B support'), + 'wpa3-192': has_ap_eap192 || _('Requires hostapd with EAP Suite-B support'), + 'owe': has_ap_owe || _('Requires hostapd with OWE support') + }, + 'sta': { + 'wep-open': has_sta_wep || _('Requires wpa-supplicant with WEP support'), + 'wep-shared': has_sta_wep || _('Requires wpa-supplicant with WEP support'), + 'psk': has_supplicant || _('Requires wpa-supplicant'), + 'psk2': has_supplicant || _('Requires wpa-supplicant'), + 'psk-mixed': has_supplicant || _('Requires wpa-supplicant'), + 'sae': has_sta_sae || _('Requires wpa-supplicant with SAE support'), + 'sae-mixed': has_sta_sae || _('Requires wpa-supplicant with SAE support'), + 'wpa': has_sta_eap || _('Requires wpa-supplicant with EAP support'), + 'wpa2': has_sta_eap || _('Requires wpa-supplicant with EAP support'), + 'wpa3': has_sta_eap192 || _('Requires wpa-supplicant with EAP Suite-B support'), + 'wpa3-mixed': has_sta_eap192 || _('Requires wpa-supplicant with EAP Suite-B support'), + 'wpa3-192': has_sta_eap192 || _('Requires wpa-supplicant with EAP Suite-B support'), + 'owe': has_sta_owe || _('Requires wpa-supplicant with OWE support') + }, + 'adhoc': { + 'wep-open': true, + 'wep-shared': true, + 'psk': has_supplicant || _('Requires wpa-supplicant'), + 'psk2': has_supplicant || _('Requires wpa-supplicant'), + 'psk-mixed': has_supplicant || _('Requires wpa-supplicant'), + }, + 'mesh': { + 'sae': has_sta_sae || _('Requires wpa-supplicant with SAE support') + }, + 'ahdemo': { + 'wep-open': true, + 'wep-shared': true + }, + 'wds': { + 'wep-open': true, + 'wep-shared': true + } + }; + + encr.crypto_support['ap-wds'] = encr.crypto_support['ap']; + encr.crypto_support['sta-wds'] = encr.crypto_support['sta']; + + encr.validate = function(section_id, value) { + const modeopt = this.section.children.filter(function(o) { return o.option == 'mode'; })[0]; + const modeval = modeopt.formvalue(section_id); + const modetitle = modeopt.vallist[modeopt.keylist.indexOf(modeval)]; + const enctitle = this.vallist[this.keylist.indexOf(value)]; + + if (value == 'none') + return true; + + if (!L.isObject(this.crypto_support[modeval]) || !this.crypto_support[modeval].hasOwnProperty(value)) + return _('The selected %s mode is incompatible with %s encryption').format(modetitle, enctitle); + + return this.crypto_support[modeval][value]; + }; + } + else if (hwtype == 'broadcom') { + crypto_modes.push(['psk2', 'WPA2-PSK', 33]); + crypto_modes.push(['psk+psk2', 'WPA-PSK/WPA2-PSK Mixed Mode', 22]); + crypto_modes.push(['psk', 'WPA-PSK', 12]); + crypto_modes.push(['wep-open', _('WEP Open System'), 11]); + crypto_modes.push(['wep-shared', _('WEP Shared Key'), 10]); + } + + crypto_modes.push(['none', _('No Encryption'), 0]); + + crypto_modes.sort(function(a, b) { return b[2] - a[2]; }); + + crypto_modes.forEach(crypto_mode => { + const security_level = (crypto_mode[2] >= 30) ? _('strong security') + : (crypto_mode[2] >= 20) ? _('medium security') + : (crypto_mode[2] >= 10) ? _('weak security') : _('open network'); + + encr.value(crypto_mode[0], '%s (%s)'.format(crypto_mode[1], security_level)); + }); + + + o = ss.taboption('encryption', form.Flag, 'ppsk', _('Enable Private PSK (PPSK)'), _('Private Pre-Shared Key (PPSK) allows the use of different Pre-Shared Key for each STA MAC address. Private MAC PSKs are stored on the RADIUS server.')); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk', 'psk2', 'psk+psk2', 'psk-mixed'] }); + + o = ss.taboption('encryption', form.Value, 'auth_server', _('RADIUS Authentication Server')); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk', 'psk2', 'psk+psk2', 'psk-mixed'], ppsk: ['1'] }); + o.rmempty = true; + o.datatype = 'host(0)'; + + o = ss.taboption('encryption', form.Value, 'auth_port', _('RADIUS Authentication Port')); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk', 'psk2', 'psk+psk2', 'psk-mixed'], ppsk: ['1'] }); + o.rmempty = true; + o.datatype = 'port'; + o.placeholder = '1812'; + + o = ss.taboption('encryption', form.Value, 'auth_secret', _('RADIUS Authentication Secret')); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk', 'psk2', 'psk+psk2', 'psk-mixed'], ppsk: ['1'] }); + o.rmempty = true; + o.password = true; + + o = ss.taboption('encryption', form.Value, 'acct_server', _('RADIUS Accounting Server')); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + o.rmempty = true; + o.datatype = 'host(0)'; + + o = ss.taboption('encryption', form.Value, 'acct_port', _('RADIUS Accounting Port')); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + o.rmempty = true; + o.datatype = 'port'; + o.placeholder = '1813'; + + o = ss.taboption('encryption', form.Value, 'acct_secret', _('RADIUS Accounting Secret')); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + o.rmempty = true; + o.password = true; + + /* extra RADIUS settings start */ + const attr_validate = function(section_id, value) { + if (!value) + return true; + + if (!/^[0-9]+(:s:.+|:d:[0-9]+|:x:([0-9a-zA-Z]{2})+)?$/.test(value) ) + return _('Must be in %s format.').format('[:]'); + + return true; + }; + + const req_attr_syntax = _('Format:') + '<attr_id>[:<syntax:value>]' + '
' + + 'syntax: s = %s; '.format(_('string (UTF-8)')) + 'd = %s; '.format(_('integer')) + 'x = %s'.format(_('octet string')); + + /* https://w1.fi/cgit/hostap/commit/?id=af35e7af7f8bb1ca9f0905b4074fb56a264aa12b */ + o = ss.taboption('encryption', form.DynamicList, 'radius_auth_req_attr', _('RADIUS Access-Request attributes'), + _('Attributes to add/replace in each request.') + '
' + req_attr_syntax ); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + o.rmempty = true; + o.validate = attr_validate; + o.placeholder = '126:s:Operator'; + + o = ss.taboption('encryption', form.DynamicList, 'radius_acct_req_attr', _('RADIUS Accounting-Request attributes'), + _('Attributes to add/replace in each request.') + '
' + req_attr_syntax ); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + o.rmempty = true; + o.validate = attr_validate; + o.placeholder = '77:x:74657374696e67'; + + o = ss.taboption('encryption', form.ListValue, 'dynamic_vlan', _('RADIUS Dynamic VLAN Assignment'), _('Required: Rejects auth if RADIUS server does not provide appropriate VLAN attributes.')); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk', 'psk2', 'psk+psk2', 'psk-mixed'], ppsk: ['1'] }); + o.value('0', _('Disabled')); + o.value('1', _('Optional')); + o.value('2', _('Required')); + o.write = function (section_id, value) { + return this.super('write', [section_id, (value == 0) ? null: value]); + }; + + o = ss.taboption('encryption', form.Flag, 'per_sta_vif', _('RADIUS Per STA VLAN'), _('Each STA is assigned its own AP_VLAN interface.')); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk', 'psk2', 'psk+psk2', 'psk-mixed'], ppsk: ['1'] }); + + //hostapd internally defaults to vlan_naming=1 even with dynamic VLAN off + o = ss.taboption('encryption', form.Flag, 'vlan_naming', _('RADIUS VLAN Naming'), _('Off: vlanXXX, e.g., vlan1. On: vlan_tagged_interface.XXX, e.g. eth0.1.')); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk', 'psk2', 'psk+psk2', 'psk-mixed'], ppsk: ['1'] }); + + o = ss.taboption('encryption', widgets.DeviceSelect, 'vlan_tagged_interface', _('RADIUS VLAN Tagged Interface'), _('E.g. eth0, eth1')); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk', 'psk2', 'psk+psk2', 'psk-mixed'], ppsk: ['1'] }); + o.size = 1; + o.rmempty = true; + o.multiple = false; + o.noaliases = true; + o.nocreate = true; + o.noinactive = true; + + o = ss.taboption('encryption', form.Value, 'vlan_bridge', _('RADIUS VLAN Bridge Naming Scheme'), _('E.g. br-vlan or brvlan.')); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk', 'psk2', 'psk+psk2', 'psk-mixed'], ppsk: ['1'] }); + o.rmempty = true; + + /* extra RADIUS settings end */ + + o = ss.taboption('encryption', form.Value, 'dae_client', _('DAE-Client'), _('Dynamic Authorization Extension client.')); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + o.rmempty = true; + o.datatype = 'host(0)'; + + o = ss.taboption('encryption', form.Value, 'dae_port', _('DAE-Port'), _('Dynamic Authorization Extension port.')); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + o.rmempty = true; + o.datatype = 'port'; + o.placeholder = '3799'; + + o = ss.taboption('encryption', form.Value, 'dae_secret', _('DAE-Secret'), _('Dynamic Authorization Extension secret.')); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + o.rmempty = true; + o.password = true; + + //WPA(1) has only WPA IE. Only >= WPA2 has RSN IE Preauth frames. + o = ss.taboption('encryption', form.Flag, 'rsn_preauth', _('RSN Preauth'), _('Robust Security Network (RSN): Allow roaming preauth for WPA2-EAP networks (and advertise it in WLAN beacons). Only works if the specified network interface is a bridge. Shortens the time-critical reassociation process.')); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa2', 'wpa3', 'wpa3-mixed'] }); + + + o = ss.taboption('encryption', form.Value, '_wpa_key', _('Key')); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk', 'psk2', 'psk+psk2', 'psk-mixed'], ppsk: ['0'] }); + add_dependency_permutations(o, { mode: ['sta', 'adhoc', 'mesh', 'sta-wds'], encryption: ['psk', 'psk2', 'psk+psk2', 'psk-mixed'] }); + o.depends('encryption', 'sae'); + o.depends('encryption', 'sae-mixed'); + o.datatype = 'wpakey'; + o.rmempty = true; + o.password = true; + + o.cfgvalue = function(section_id) { + const key = uci.get('wireless', section_id, 'key'); + return /^[1234]$/.test(key) ? null : key; + }; + + o.write = function(section_id, value) { + uci.set('wireless', section_id, 'key', value); + uci.unset('wireless', section_id, 'key1'); + uci.unset('wireless', section_id, 'key2'); + uci.unset('wireless', section_id, 'key3'); + uci.unset('wireless', section_id, 'key4'); + }; + + + o = ss.taboption('encryption', form.ListValue, '_wep_key', _('Used Key Slot')); + o.depends('encryption', 'wep-open'); + o.depends('encryption', 'wep-shared'); + o.value('1', _('Key #%d').format(1)); + o.value('2', _('Key #%d').format(2)); + o.value('3', _('Key #%d').format(3)); + o.value('4', _('Key #%d').format(4)); + + o.cfgvalue = function(section_id) { + const slot = +uci.get('wireless', section_id, 'key'); + return (slot >= 1 && slot <= 4) ? String(slot) : ''; + }; + + o.write = function(section_id, value) { + uci.set('wireless', section_id, 'key', value); + }; + + for (let slot = 1; slot <= 4; slot++) { + o = ss.taboption('encryption', form.Value, 'key%d'.format(slot), _('Key #%d').format(slot)); + o.depends('encryption', 'wep-open'); + o.depends('encryption', 'wep-shared'); + o.datatype = 'wepkey'; + o.rmempty = true; + o.password = true; + + o.write = function(section_id, value) { + if (value != null && (value.length == 5 || value.length == 13)) + value = 's:%s'.format(value); + uci.set('wireless', section_id, this.option, value); + }; + } + + + if (hwtype == 'mac80211') { + + // Probe 802.11r support (and EAP support as a proxy for Openwrt) + const has_80211r = L.hasSystemFeature('hostapd', '11r') || L.hasSystemFeature('hostapd', 'eap'); + + o = ss.taboption('roaming', form.Flag, 'ieee80211r', _('802.11r Fast Transition'), _('Enables fast roaming among access points that belong to the same Mobility Domain')); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa2', 'wpa3', 'wpa3-mixed', , 'wpa3-192'] }); + if (has_80211r) + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk2', 'psk-mixed', 'sae', 'sae-mixed'] }); + o.rmempty = true; + + o = ss.taboption('roaming', form.Value, 'nasid', _('NAS ID'), _('Used for two different purposes: RADIUS NAS ID and 802.11r R0KH-ID. Not needed with normal WPA(2)-PSK.')); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + o.depends({ ieee80211r: '1' }); + o.rmempty = true; + + o = ss.taboption('roaming', form.Value, 'mobility_domain', _('Mobility Domain'), _('4-character hexadecimal ID')); + o.depends({ ieee80211r: '1' }); + o.placeholder = '4f57'; + o.datatype = 'and(hexstring,length(4))'; + o.rmempty = true; + + o = ss.taboption('roaming', form.Value, 'reassociation_deadline', _('Reassociation Deadline'), _('time units (TUs / 1.024 ms) [1000-65535]')); + o.depends({ ieee80211r: '1' }); + o.placeholder = '1000'; + o.datatype = 'range(1000,65535)'; + o.rmempty = true; + + o = ss.taboption('roaming', form.ListValue, 'ft_over_ds', _('FT protocol')); + o.depends({ ieee80211r: '1' }); + o.value('0', _('FT over the Air')); + o.value('1', _('FT over DS')); + o.rmempty = true; + + o = ss.taboption('roaming', form.Flag, 'ft_psk_generate_local', _('Generate PMK locally'), _('When using a PSK, the PMK can be automatically generated. When enabled, the R0/R1 key options below are not applied. Disable this to use the R0 and R1 key options.')); + add_dependency_permutations(o, { ieee80211r: ['1'], mode: ['ap', 'ap-wds'], encryption: ['psk2', 'psk-mixed'] }); + o.default = o.enabled; + o.rmempty = false; + + o = ss.taboption('roaming', form.Value, 'r0_key_lifetime', _('R0 Key Lifetime'), _('minutes')); + o.depends({ ieee80211r: '1' }); + o.placeholder = '10000'; + o.datatype = 'uinteger'; + o.rmempty = true; + + o = ss.taboption('roaming', form.Value, 'r1_key_holder', _('R1 Key Holder'), _('6-octet identifier as a hex string - no colons')); + o.depends({ ieee80211r: '1' }); + o.placeholder = '00004f577274'; + o.datatype = 'and(hexstring,length(12))'; + o.rmempty = true; + + o = ss.taboption('roaming', form.Flag, 'pmk_r1_push', _('PMK R1 Push')); + o.depends({ ieee80211r: '1' }); + o.placeholder = '0'; + o.rmempty = true; + + o = ss.taboption('roaming', form.DynamicList, 'r0kh', _('External R0 Key Holder List'), _('List of R0KHs in the same Mobility Domain.
Format: MAC-address,NAS-Identifier,256-bit key as hex string.
This list is used to map R0KH-ID (NAS Identifier) to a destination MAC address when requesting PMK-R1 key from the R0KH that the STA used during the Initial Mobility Domain Association.')); + o.depends({ ieee80211r: '1' }); + o.rmempty = true; + + o = ss.taboption('roaming', form.DynamicList, 'r1kh', _('External R1 Key Holder List'), _ ('List of R1KHs in the same Mobility Domain.
Format: MAC-address,R1KH-ID as 6 octets with colons,256-bit key as hex string.
This list is used to map R1KH-ID to a destination MAC address when sending PMK-R1 key from the R0KH. This is also the list of authorized R1KHs in the MD that can request PMK-R1 keys.')); + o.depends({ ieee80211r: '1' }); + o.rmempty = true; + // End of 802.11r options + + // Probe 802.11k and 802.11v support via EAP support (full hostapd has EAP) + if (L.hasSystemFeature('hostapd', 'eap')) { + /* 802.11k settings start */ + o = ss.taboption('roaming', form.Flag, 'ieee80211k', _('802.11k RRM'), _('Radio Resource Measurement - Sends beacons to assist roaming. Not all clients support this.')); + // add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk', 'psk2', 'psk-mixed', 'sae', 'sae-mixed'] }); + o.depends('mode', 'ap'); + o.depends('mode', 'ap-wds'); + + o = ss.taboption('roaming', form.Flag, 'rrm_neighbor_report', _('Neighbour Report'), _('802.11k: Enable neighbor report via radio measurements.')); + o.depends({ ieee80211k: '1' }); + o.default = o.enabled; + + o = ss.taboption('roaming', form.Flag, 'rrm_beacon_report', _('Beacon Report'), _('802.11k: Enable beacon report via radio measurements.')); + o.depends({ ieee80211k: '1' }); + o.default = o.enabled; + /* 802.11k settings end */ + + /* 802.11v settings start */ + o = ss.taboption('roaming', form.ListValue, 'time_advertisement', _('Time advertisement'), _('802.11v: Time Advertisement in management frames.')); + o.value('0', _('Disabled')); + o.value('2', _('Enabled')); + o.write = function (section_id, value) { + return this.super('write', [section_id, (value == 2) ? value: null]); + }; + + //Pull current System TZ setting + const tz = uci.get('system', '@system[0]', 'timezone'); + o = ss.taboption('roaming', form.Value, 'time_zone', _('Time zone'), _('802.11v: Local Time Zone Advertisement in management frames.')); + o.value(tz); + o.rmempty = true; + + o = ss.taboption('roaming', form.Flag, 'wnm_sleep_mode', _('WNM Sleep Mode'), _('802.11v: Wireless Network Management (WNM) Sleep Mode (extended sleep mode for stations).')); + o.rmempty = true; + + /* wnm_sleep_mode_no_keys: https://git.openwrt.org/?p=openwrt/openwrt.git;a=commitdiff;h=bf98faaac8ed24cf7d3d93dd4fcd7304d109363b */ + o = ss.taboption('roaming', form.Flag, 'wnm_sleep_mode_no_keys', _('WNM Sleep Mode Fixes'), _('802.11v: Wireless Network Management (WNM) Sleep Mode Fixes: Prevents reinstallation attacks.')); + o.rmempty = true; + + o = ss.taboption('roaming', form.Flag, 'bss_transition', _('BSS Transition'), _('802.11v: Basic Service Set (BSS) transition management.')); + o.rmempty = true; + + /* in master, but not 21.02.1: proxy_arp */ + o = ss.taboption('roaming', form.Flag, 'proxy_arp', _('ProxyARP'), _('802.11v: Proxy ARP enables non-AP STA to remain in power-save for longer.')); + o.rmempty = true; + + /* TODO: na_mcast_to_ucast is missing: needs adding to hostapd.sh - nice to have */ + } + /* 802.11v settings end */ + } + + if (hwtype == 'mac80211') { + o = ss.taboption('encryption', form.ListValue, 'eap_type', _('EAP-Method')); + o.value('tls', 'TLS'); + o.value('ttls', 'TTLS'); + o.value('peap', 'PEAP'); + o.value('fast', 'FAST'); + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + + o = ss.taboption('encryption', form.Flag, 'ca_cert_usesystem', _('Use system certificates'), _("Validate server certificate using built-in system CA bundle,
requires the \"ca-bundle\" package")); + o.enabled = '1'; + o.disabled = '0'; + o.default = o.disabled; + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + o.validate = function(section_id, value) { + if (value == '1' && !L.hasSystemFeature('cabundle')) { + return _("This option cannot be used because the ca-bundle package is not installed."); + } + return true; + }; + + o = ss.taboption('encryption', form.FileUpload, 'ca_cert', _('Path to CA-Certificate')); + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'], ca_cert_usesystem: ['0'] }); + + o = ss.taboption('encryption', form.Value, 'subject_match', _('Certificate constraint (Subject)'), _("Certificate constraint substring - e.g. /CN=wifi.mycompany.com
See `logread -f` during handshake for actual values")); + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + + o = ss.taboption('encryption', form.DynamicList, 'altsubject_match', _('Certificate constraint (SAN)'), _("Certificate constraint(s) via Subject Alternate Name values
(supported attributes: EMAIL, DNS, URI) - e.g. DNS:wifi.mycompany.com")); + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + + o = ss.taboption('encryption', form.DynamicList, 'domain_match', _('Certificate constraint (Domain)'), _("Certificate constraint(s) against DNS SAN values (if available)
or Subject CN (exact match)")); + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + + o = ss.taboption('encryption', form.DynamicList, 'domain_suffix_match', _('Certificate constraint (Wildcard)'), _("Certificate constraint(s) against DNS SAN values (if available)
or Subject CN (suffix match)")); + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'] }); + + o = ss.taboption('encryption', form.FileUpload, 'client_cert', _('Path to Client-Certificate')); + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'], eap_type: ['tls'] }); + + o = ss.taboption('encryption', form.FileUpload, 'priv_key', _('Path to Private Key')); + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'], eap_type: ['tls'] }); + + o = ss.taboption('encryption', form.Value, 'priv_key_pwd', _('Password of Private Key')); + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'], eap_type: ['tls'] }); + o.password = true; + + o = ss.taboption('encryption', form.ListValue, 'auth', _('Authentication')); + o.value('PAP', 'PAP'); + o.value('CHAP', 'CHAP'); + o.value('MSCHAP', 'MSCHAP'); + o.value('MSCHAPV2', 'MSCHAPv2'); + o.value('EAP-GTC', 'EAP-GTC'); + o.value('EAP-MD5', 'EAP-MD5'); + o.value('EAP-MSCHAPV2', 'EAP-MSCHAPv2'); + o.value('EAP-TLS', 'EAP-TLS'); + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'], eap_type: ['fast', 'peap', 'ttls'] }); + + o.validate = function(section_id, value) { + const eo = this.section.children.filter(function(o) { return o.option == 'eap_type'; })[0]; + const ev = eo.formvalue(section_id); + + if (ev != 'ttls' && (value == 'PAP' || value == 'CHAP' || value == 'MSCHAP' || value == 'MSCHAPV2')) + return _('This authentication type is not applicable to the selected EAP method.'); + + return true; + }; + + o = ss.taboption('encryption', form.Flag, 'ca_cert2_usesystem', _('Use system certificates for inner-tunnel'), _("Validate server certificate using built-in system CA bundle,
requires the \"ca-bundle\" package")); + o.enabled = '1'; + o.disabled = '0'; + o.default = o.disabled; + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'], auth: ['EAP-TLS'] }); + o.validate = function(section_id, value) { + if (value == '1' && !L.hasSystemFeature('cabundle')) { + return _("This option cannot be used because the ca-bundle package is not installed."); + } + return true; + }; + + o = ss.taboption('encryption', form.FileUpload, 'ca_cert2', _('Path to inner CA-Certificate')); + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'], auth: ['EAP-TLS'], ca_cert2_usesystem: ['0'] }); + + o = ss.taboption('encryption', form.Value, 'subject_match2', _('Inner certificate constraint (Subject)'), _("Certificate constraint substring - e.g. /CN=wifi.mycompany.com
See `logread -f` during handshake for actual values")); + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'], auth: ['EAP-TLS'] }); + + o = ss.taboption('encryption', form.DynamicList, 'altsubject_match2', _('Inner certificate constraint (SAN)'), _("Certificate constraint(s) via Subject Alternate Name values
(supported attributes: EMAIL, DNS, URI) - e.g. DNS:wifi.mycompany.com")); + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'], auth: ['EAP-TLS'] }); + + o = ss.taboption('encryption', form.DynamicList, 'domain_match2', _('Inner certificate constraint (Domain)'), _("Certificate constraint(s) against DNS SAN values (if available)
or Subject CN (exact match)")); + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'], auth: ['EAP-TLS'] }); + + o = ss.taboption('encryption', form.DynamicList, 'domain_suffix_match2', _('Inner certificate constraint (Wildcard)'), _("Certificate constraint(s) against DNS SAN values (if available)
or Subject CN (suffix match)")); + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'], auth: ['EAP-TLS'] }); + + o = ss.taboption('encryption', form.FileUpload, 'client_cert2', _('Path to inner Client-Certificate')); + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'], auth: ['EAP-TLS'] }); + + o = ss.taboption('encryption', form.FileUpload, 'priv_key2', _('Path to inner Private Key')); + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'], auth: ['EAP-TLS'] }); + + o = ss.taboption('encryption', form.Value, 'priv_key2_pwd', _('Password of inner Private Key')); + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'], auth: ['EAP-TLS'] }); + o.password = true; + + o = ss.taboption('encryption', form.Value, 'identity', _('Identity')); + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'], eap_type: ['fast', 'peap', 'tls', 'ttls'] }); + + o = ss.taboption('encryption', form.Value, 'anonymous_identity', _('Anonymous Identity')); + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'], eap_type: ['fast', 'peap', 'tls', 'ttls'] }); + + o = ss.taboption('encryption', form.Value, 'password', _('Password')); + add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed', 'wpa3-192'], eap_type: ['fast', 'peap', 'ttls'] }); + o.password = true; + + + if (hwtype == 'mac80211') { + // ieee802.11w options + o = ss.taboption('encryption', form.ListValue, 'ieee80211w', _('802.11w Management Frame Protection'), _("Note: Some wireless drivers do not fully support 802.11w. E.g. mwlwifi may have problems")); + o.value('0', _('Disabled')); + o.value('1', _('Optional')); + o.value('2', _('Required')); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds', 'sta', 'sta-wds'], encryption: ['owe', 'psk2', 'psk-mixed', 'sae', 'sae-mixed', 'wpa2', 'wpa3', 'wpa3-mixed'] }); + + o.defaults = { + '2': [{ encryption: 'sae' }, { encryption: 'owe' }, { encryption: 'wpa3' }, { encryption: 'wpa3-mixed' }], + '1': [{ encryption: 'sae-mixed'}], + '0': [] + }; + + o.write = function(section_id, value) { + if (value != this.default) + return form.ListValue.prototype.write.call(this, section_id, value); + else + return form.ListValue.prototype.remove.call(this, section_id); + }; + + o = ss.taboption('encryption', form.Value, 'ieee80211w_max_timeout', _('802.11w maximum timeout'), _('802.11w Association SA Query maximum timeout')); + o.depends('ieee80211w', '1'); + o.depends('ieee80211w', '2'); + o.datatype = 'uinteger'; + o.placeholder = '1000'; + o.rmempty = true; + + o = ss.taboption('encryption', form.Value, 'ieee80211w_retry_timeout', _('802.11w retry timeout'), _('802.11w Association SA Query retry timeout')); + o.depends('ieee80211w', '1'); + o.depends('ieee80211w', '2'); + o.datatype = 'uinteger'; + o.placeholder = '201'; + o.rmempty = true; + + if (L.hasSystemFeature('hostapd', 'ocv') || L.hasSystemFeature('wpasupplicant', 'ocv')) { + o = ss.taboption('encryption', form.ListValue, 'ocv', _('Operating Channel Validation'), _("Note: Workaround mode allows a STA that claims OCV capability to connect even if the STA doesn't send OCI or negotiate PMF.")); + o.value('0', _('Disabled')); + o.value('1', _('Enabled')); + o.value('2', _('Enabled (workaround mode)')); + o.default = '0'; + o.depends('ieee80211w', '1'); + o.depends('ieee80211w', '2'); + + o.validate = function(section_id, value) { + const modeopt = this.section.children.filter(function(o) { return o.option == 'mode'; })[0]; + const modeval = modeopt.formvalue(section_id); + + if ((value == '2') && ((modeval == 'sta') || (modeval == 'sta-wds'))) { + return _('Workaround mode can only be used when acting as an access point.'); + } + + return true; + }; + } + + o = ss.taboption('encryption', form.Flag, 'wpa_disable_eapol_key_retries', _('Enable key reinstallation (KRACK) countermeasures'), _('Complicates key reinstallation attacks on the client side by disabling retransmission of EAPOL-Key frames that are used to install keys. This workaround might cause interoperability issues and reduced robustness of key negotiation especially in environments with heavy traffic load.')); + add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk2', 'psk-mixed', 'sae', 'sae-mixed', 'wpa2', 'wpa3', 'wpa3-mixed'] }); + + if (L.hasSystemFeature('hostapd', 'wps') && L.hasSystemFeature('wpasupplicant')) { + o = ss.taboption('encryption', form.Flag, 'wps_pushbutton', _('Enable WPS pushbutton, requires WPA(2)-PSK/WPA3-SAE')); + o.enabled = '1'; + o.disabled = '0'; + o.default = o.disabled; + o.depends('encryption', 'psk'); + o.depends('encryption', 'psk2'); + o.depends('encryption', 'psk-mixed'); + o.depends('encryption', 'sae'); + o.depends('encryption', 'sae-mixed'); + } + } + } + }); + }; + + s.handleRemove = function(section_id, ev) { + document.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(section_id)).style.opacity = 0.5; + return form.TypedSection.prototype.handleRemove.apply(this, [section_id, ev]); + }; + + s.handleScan = function(radioDev, ev) { + const table = E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th col-2 middle center' }, _('Signal')), + E('th', { 'class': 'th col-4 middle left' }, _('SSID')), + E('th', { 'class': 'th col-2 middle center hide-xs' }, _('Channel')), + E('th', { 'class': 'th col-2 middle left hide-xs' }, _('Mode')), + E('th', { 'class': 'th col-3 middle left hide-xs' }, _('BSSID')), + E('th', { 'class': 'th col-3 middle left' }, _('Encryption')), + E('th', { 'class': 'th cbi-section-actions right' }, ' '), + ]) + ]); + + const stop = E('button', { + 'class': 'btn', + 'click': L.bind(this.handleScanStartStop, this), + 'style': 'display:none', + 'data-state': 'stop' + }, _('Stop refresh')); + + cbi_update_table(table, [], E('em', { class: 'spinning' }, _('Starting wireless scan...'))); + + const md = ui.showModal(_('Join Network: Wireless Scan'), [ + table, + E('div', { 'class': 'right' }, [ + stop, + ' ', + E('button', { + 'class': 'btn', + 'click': L.bind(this.handleScanAbort, this) + }, _('Dismiss')) + ]) + ]); + + md.style.maxWidth = '90%'; + md.style.maxHeight = 'none'; + + this.pollFn = L.bind(this.handleScanRefresh, this, radioDev, {}, table, stop); + + poll.add(this.pollFn); + poll.start(); + }; + + s.handleScanRefresh = function(radioDev, scanCache, table, stop) { + return radioDev.getScanList().then(L.bind(function(results) { + const rows = []; + + for (let r of results) + scanCache[r.bssid] = r; + + for (let k in scanCache) + if (scanCache[k].stale) + results.push(scanCache[k]); + + results.sort(function(a, b) { + const diff = (b.quality - a.quality) || (a.channel - b.channel); + + if (diff) + return diff; + + if (a.ssid < b.ssid) + return -1; + else if (a.ssid > b.ssid) + return 1; + + if (a.bssid < b.bssid) + return -1; + else if (a.bssid > b.bssid) + return 1; + return 0; + }); + + results.forEach(res => { + const qv = res?.quality ?? 0; + const qm = res?.quality_max ?? 0; + const q = (qv > 0 && qm > 0) ? Math.floor((100 / qm) * qv) : 0; + const s = res.stale ? 'opacity:0.5' : ''; + const ssid = (typeof res.ssid === 'string' && res.ssid.length > 0) ? document.createTextNode(`${res?.ssid}`) : null; + + rows.push([ + E('span', { 'style': s }, render_signal_badge(q, res?.signal, res?.noise)), + E('span', { 'style': s }, ssid ?? E('em', _('hidden'))), + E('span', { 'style': s }, `${res?.channel}`), + E('span', { 'style': s }, `${res?.mode}`), + E('span', { 'style': s }, `${res?.bssid}`), + E('span', { 'style': s }, `${network.formatWifiEncryption(res?.encryption)}`), + E('div', { 'class': 'right' }, E('button', { + 'class': 'cbi-button cbi-button-action important', + 'click': ui.createHandlerFn(this, 'handleJoin', radioDev, res) + }, _('Join Network'))) + ]); + + res.stale = true; + }); + + cbi_update_table(table, rows); + + stop.disabled = false; + stop.style.display = ''; + stop.classList.remove('spinning'); + }, this)); + }; + + s.handleScanStartStop = function(ev) { + const btn = ev.currentTarget; + + if (btn.getAttribute('data-state') == 'stop') { + if (this.pollFn) + poll.remove(this.pollFn); + btn.firstChild.data = _('Start refresh'); + btn.setAttribute('data-state', 'start'); + } + else { + poll.add(this.pollFn); + btn.firstChild.data = _('Stop refresh'); + btn.setAttribute('data-state', 'stop'); + btn.classList.add('spinning'); + btn.disabled = true; + } + }; + + s.handleScanAbort = function(ev) { + const md = dom.parent(ev.target, 'div[aria-modal="true"]'); + if (md) { + md.style.maxWidth = ''; + md.style.maxHeight = ''; + } + + ui.hideModal(); + if (this.pollFn) + poll.remove(this.pollFn); + this.pollFn = null; + }; + + s.handleJoinConfirm = function(radioDev, bss, form, ev) { + const nameopt = L.toArray(form.lookupOption('name', '_new_'))[0]; + const passopt = L.toArray(form.lookupOption('password', '_new_'))[0]; + const ssidopt = L.toArray(form.lookupOption('ssid', '_new_'))[0]; + const bssidopt = L.toArray(form.lookupOption('bssid', '_new_'))[0]; + const zoneopt = L.toArray(form.lookupOption('zone', '_new_'))[0]; + const replopt = L.toArray(form.lookupOption('replace', '_new_'))[0]; + const nameval = (nameopt && nameopt.isValid('_new_')) ? nameopt.formvalue('_new_') : null; + const passval = (passopt && passopt.isValid('_new_')) ? passopt.formvalue('_new_') : null; + const ssidval = (ssidopt && ssidopt.isValid('_new_')) ? ssidopt.formvalue('_new_') : null; + const bssidval = (bssidopt && bssidopt.isValid('_new_')) ? bssidopt.formvalue('_new_') : null; + const zoneval = zoneopt ? zoneopt.formvalue('_new_') : null; + const enc = L.isObject(bss.encryption) ? bss.encryption : null; + const is_wep = (enc && Array.isArray(enc.wep)); + const is_psk = (enc && Array.isArray(enc.wpa) && L.toArray(enc.authentication).filter(function(a) { return a == 'psk'; }).length > 0); + const is_sae = (enc && Array.isArray(enc.wpa) && L.toArray(enc.authentication).filter(function(a) { return a == 'sae'; }).length > 0); + + if (nameval == null || (passopt && passval == null)) + return; + + let section_id = null; + + return this.map.save(function() { + const wifi_sections = uci.sections('wireless', 'wifi-iface'); + + if (replopt.formvalue('_new_') == '1') { + for (let ws of wifi_sections) + if (ws.device == radioDev.getName()) + uci.remove('wireless', ws['.name']); + } + + if (uci.get('wireless', radioDev.getName(), 'disabled') == '1') { + for (let ws of wifi_sections) + if (ws.device == radioDev.getName()) + uci.set('wireless', ws['.name'], 'disabled', '1'); + + uci.unset('wireless', radioDev.getName(), 'disabled'); + } + + const htmodes = radioDev.getHTModes(); + + if (bss.vht_operation && htmodes && htmodes.indexOf('VHT20') !== -1) { + for (let w = bss.vht_operation.channel_width; w >= 20; w /= 2) { + if (htmodes.indexOf('VHT'+w) !== -1) { + uci.set('wireless', radioDev.getName(), 'htmode', 'VHT'+w); + break; + } + } + } + else if (bss.ht_operation && htmodes && htmodes.indexOf('HT20') !== -1) { + const w = (bss.ht_operation.secondary_channel_offset == 'no secondary') ? 20 : 40; + uci.set('wireless', radioDev.getName(), 'htmode', 'HT'+w); + } + else { + uci.unset('wireless', radioDev.getName(), 'htmode'); + } + + uci.set('wireless', radioDev.getName(), 'channel', bss.channel); + + section_id = next_free_sid(wifi_sections.length); + + uci.add('wireless', 'wifi-iface', section_id); + uci.set('wireless', section_id, 'device', radioDev.getName()); + uci.set('wireless', section_id, 'mode', (bss.mode == 'Ad-Hoc') ? 'adhoc' : 'sta'); + uci.set('wireless', section_id, 'network', nameval); + + if (bss.ssid != null) { + uci.set('wireless', section_id, 'ssid', bss.ssid); + + if (bssidval == '1') + uci.set('wireless', section_id, 'bssid', bss.bssid); + } + else if (bss.bssid != null) { + uci.set('wireless', section_id, 'bssid', bss.bssid); + } + + if (ssidval != null) + uci.set('wireless', section_id, 'ssid', ssidval); + + if (is_sae) { + uci.set('wireless', section_id, 'encryption', 'sae'); + uci.set('wireless', section_id, 'key', passval); + } + else if (is_psk) { + for (let i = enc.wpa.length - 1; i >= 0; i--) { + if (enc.wpa[i] == 2) { + uci.set('wireless', section_id, 'encryption', 'psk2'); + break; + } + else if (enc.wpa[i] == 1) { + uci.set('wireless', section_id, 'encryption', 'psk'); + break; + } + } + + uci.set('wireless', section_id, 'key', passval); + } + else if (is_wep) { + uci.set('wireless', section_id, 'encryption', 'wep-open'); + uci.set('wireless', section_id, 'key', '1'); + uci.set('wireless', section_id, 'key1', passval); + } + else { + uci.set('wireless', section_id, 'encryption', 'none'); + } + + return network.addNetwork(nameval, { proto: 'dhcp' }).then(function(net) { + firewall.deleteNetwork(net.getName()); + + const zonePromise = zoneval ? + firewall.getZone(zoneval).then(function(zone) { return zone || firewall.addZone(zoneval); }) + : Promise.resolve(); + + return zonePromise.then(function(zone) { + if (zone) + zone.addNetwork(net.getName()); + }); + }); + }).then(L.bind(function() { + ui.showModal(null, E('p', { 'class': 'spinning' }, [ _('Loading data…') ])); + + return this.renderMoreOptionsModal(section_id); + }, this)); + }; + + s.handleJoin = function(radioDev, bss, ev) { + if (this.pollFn) + poll.remove(this.pollFn); + const m2 = new form.Map('wireless'); + const s2 = m2.section(form.NamedSection, '_new_'); + const enc = L.isObject(bss.encryption) ? bss.encryption : null; + const is_wep = (enc && Array.isArray(enc.wep)); + const is_psk = (enc && Array.isArray(enc.wpa) && L.toArray(enc.authentication).filter(function(a) { return a == 'psk' || a == 'sae'; })); + let replace, passphrase, name, bssid, zone; + + function nameUsed(name) { + const s = uci.get('network', name); + if (s != null && s['.type'] != 'interface') + return true; + + const net = (s != null) ? network.instantiateNetwork(name) : null; + return (net != null && !net.isEmpty()); + } + + s2.render = function() { + return Promise.all([ + {}, + this.renderUCISection('_new_') + ]).then(this.renderContents.bind(this)); + }; + + if (bss.ssid == null) { + name = s2.option(form.Value, 'ssid', _('Network SSID'), _('The correct SSID must be manually specified when joining a hidden wireless network')); + name.rmempty = false; + } + + replace = s2.option(form.Flag, 'replace', _('Replace wireless configuration'), _('Check this option to delete the existing networks from this radio.')); + + name = s2.option(form.Value, 'name', _('Name of the new network'), + _('Name for OpenWrt network configuration. (No relation to wireless network name/SSID)') + '
' + + _('The allowed characters are: A-Z, a-z, 0-9 and _')); + name.datatype = 'uciname'; + name.default = 'wwan'; + name.rmempty = false; + name.validate = function(section_id, value) { + if (nameUsed(value)) + return _('The network name is already used'); + + return true; + }; + + for (let i = 2; nameUsed(name.default); i++) + name.default = 'wwan%d'.format(i); + + if (is_wep || is_psk) { + passphrase = s2.option(form.Value, 'password', is_wep ? _('WEP passphrase') : _('WPA passphrase'), _('Specify the secret encryption key here.')); + passphrase.datatype = is_wep ? 'wepkey' : 'wpakey'; + passphrase.password = true; + passphrase.rmempty = false; + } + + if (bss.ssid != null) { + bssid = s2.option(form.Flag, 'bssid', _('Lock to BSSID'), _('Instead of joining any network with a matching SSID, only connect to the BSSID %h.').format(bss.bssid)); + bssid.default = '0'; + } + + zone = s2.option(widgets.ZoneSelect, 'zone', _('Create / Assign firewall-zone'), _('Choose the firewall zone you want to assign to this interface. Select unspecified to remove the interface from the associated zone or fill out the custom field to define a new zone and attach the interface to it.')); + zone.default = 'wan'; + + return m2.render().then(L.bind(function(nodes) { + ui.showModal(_('Joining Network: %q').replace(/%q/, '"%h"'.format(bss.ssid)), [ + nodes, + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, _('Cancel')), ' ', + E('button', { + 'class': 'cbi-button cbi-button-positive important', + 'click': ui.createHandlerFn(this, 'handleJoinConfirm', radioDev, bss, m2) + }, _('Submit')) + ]) + ], 'cbi-modal').querySelector('[id="%s"] input[class][type]'.format((passphrase || name).cbid('_new_'))).focus(); + }, this)); + }; + + s.handleAdd = function(radioDev, ev) { + const section_id = next_free_sid(uci.sections('wireless', 'wifi-iface').length); + + uci.unset('wireless', radioDev.getName(), 'disabled'); + + uci.add('wireless', 'wifi-iface', section_id); + uci.set('wireless', section_id, 'device', radioDev.getName()); + uci.set('wireless', section_id, 'mode', 'ap'); + uci.set('wireless', section_id, 'ssid', 'ImmortalWrt'); + uci.set('wireless', section_id, 'encryption', 'none'); + + m.addedSection = section_id; + return this.renderMoreOptionsModal(section_id); + }; + + o = s.option(form.DummyValue, '_badge'); + o.modalonly = false; + o.textvalue = function(section_id) { + const inst = this.section.lookupRadioOrNetwork(section_id); + const node = E('div', { 'class': 'center' }); + + if (inst.getWifiNetworks) + node.appendChild(render_radio_badge(inst)); + else + node.appendChild(render_network_badge(inst)); + + return node; + }; + + o = s.option(form.DummyValue, '_stat'); + o.modalonly = false; + o.textvalue = function(section_id) { + const inst = this.section.lookupRadioOrNetwork(section_id); + + if (inst.getWifiNetworks) + return render_radio_status(inst, this.section.wifis.filter(function(e) { + return (e.getWifiDeviceName() == inst.getName()); + })); + else + return render_network_status(inst); + }; + + return m.render().then(L.bind(function(m, nodes) { + poll.add(L.bind(function() { + // v30: also fetch live iwinfo data per interface (mt_wifi driver + // does not populate ubus iwinfo field). Result is cached in + // IW_ESSID and used by render_network_status / render_modal_status. + network.getWifiDevices().then(L.bind(function(devs){ + devs.forEach(function(dev){ + dev.getWifiNetworks().then(L.bind(function(nets){ + nets.forEach(function(net){ + const ifname = net.getIfname(); + if (!ifname) return; + fs.exec('/usr/bin/iwinfo', [ifname, 'info']).then(L.bind(function(out){ + const m = out && out.match(/ESSID: "([^"]*)"/); + if (m) IW_ESSID[ifname] = m[1]; + }, this)).catch(function(){}); + })); + }, this)); + }, this)); + + const tasks = [ network.getHostHints(), network.getWifiDevices() ]; + + m?.children[0]?.cfgsections?.().forEach(s => { + const row = nodes.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(s)); + const dsc = row.querySelector('[data-name="_stat"] > div'); + const btns = row.querySelectorAll('.cbi-section-actions button'); + + // v29: block auto wifi restart after LuCI save. Reboot required. + if (dsc.getAttribute('restart') == '') { + dsc.setAttribute('restart', '1'); + ui.addNotification(null, E('p', _('Configuration saved. Reboot router to apply wireless changes.'))); + btns[0].classList.remove('spinning'); + btns[0].disabled = false; + dsc.removeAttribute('restart'); + } else if (dsc.getAttribute('restart') == '1') { + dsc.removeAttribute('restart'); + btns[0].classList.remove('spinning'); + btns[0].disabled = false; + } + else if (dsc.getAttribute('restart') == '1') { + dsc.removeAttribute('restart'); + btns[0].classList.remove('spinning'); + btns[0].disabled = false; + } + }); + + return Promise.all(tasks) + .then(L.bind(function(hosts_radios) { + const tasks = []; + + hosts_radios[1].forEach(r => tasks.push(r.getWifiNetworks()) ); + + return Promise.all(tasks).then(function(data) { + hosts_radios[2] = []; + + for (let d of data) + hosts_radios[2].push.apply(hosts_radios[2], d); + + return hosts_radios; + }); + }, network)) + .then(L.bind(function(hosts_radios_wifis) { + const tasks = []; + + hosts_radios_wifis[2].forEach(hrw => tasks.push(hrw.getAssocList()) ); + + return Promise.all(tasks).then(function(data) { + hosts_radios_wifis[3] = []; + + for (let i = 0; i < data.length; i++) { + const wifiNetwork = hosts_radios_wifis[2][i]; + const radioDev = hosts_radios_wifis[1].filter(function(d) { return d.getName() == wifiNetwork.getWifiDeviceName(); })[0]; + + for (let dy of data[i]) + hosts_radios_wifis[3].push(Object.assign({ radio: radioDev, network: wifiNetwork }, dy)); + } + + return hosts_radios_wifis; + }); + }, network)) + .then(L.bind(function(zones, data) { + data.push(zones); + return data; + }, network, zones)) + .then(L.bind(this.poll_status, this, nodes)); + }, this), 5); + + const table = E('table', { 'class': 'table assoclist', 'id': 'wifi_assoclist_table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th nowrap' }, _('Network')), + E('th', { 'class': 'th hide-xs' }, _('MAC address')), + E('th', { 'class': 'th' }, _('Host')), + E('th', { 'class': 'th' }, _('Signal / Noise')), + E('th', { 'class': 'th' }, _('RX Rate / TX Rate')) + ]) + ]); + + cbi_update_table(table, [], E('em', { 'class': 'spinning' }, _('Collecting data...'))); + + return E([ nodes, E('h3', _('Associated Stations')), table ]); + }, this, m)); + }, + + handleReset: null +}); diff --git a/immortalwrt-patches/files/etc/config/dhcp b/immortalwrt-patches/files/etc/config/dhcp new file mode 100644 index 0000000..4fbb9ce --- /dev/null +++ b/immortalwrt-patches/files/etc/config/dhcp @@ -0,0 +1,35 @@ +config dnsmasq + option domainneeded '1' + option boguspriv '1' + option filterwin2k '0' + option localise_queries '1' + option rebind_protection '1' + option rebind_localhost '1' + option local '/lan/' + option domain 'lan' + option expandhosts '1' + option nonegcache '0' + option cachesize '1000' + option authoritative '1' + option readethers '1' + option leasefile '/tmp/dhcp.leases' + option resolvfile '/tmp/resolv.conf.d/resolv.conf.auto' + option nonwildcard '1' + option localservice '1' + option ednspacket_max '1232' + option filter_aaaa '0' + option filter_a '0' + +config dhcp 'lan' + option interface 'lan' + option ignore '1' + +config dhcp 'wan' + option interface 'wan' + option ignore '1' + +config odhcpd 'odhcpd' + option maindhcp '0' + option leasefile '/tmp/hosts/odhcpd' + option leasetrigger '/usr/sbin/odhcpd-update' + option loglevel '4' diff --git a/immortalwrt-patches/files/etc/config/dropbear b/immortalwrt-patches/files/etc/config/dropbear new file mode 100644 index 0000000..babbdb3 --- /dev/null +++ b/immortalwrt-patches/files/etc/config/dropbear @@ -0,0 +1,4 @@ +config dropbear + option PasswordAuth 'on' + option RootPasswordAuth 'on' + option Port '22' diff --git a/immortalwrt-patches/files/etc/config/firewall b/immortalwrt-patches/files/etc/config/firewall new file mode 100644 index 0000000..77c6992 --- /dev/null +++ b/immortalwrt-patches/files/etc/config/firewall @@ -0,0 +1,122 @@ +config defaults + option input 'REJECT' + option output 'ACCEPT' + option forward 'REJECT' + option synflood_protect '1' + option fullcone '1' + option flow_offloading '0' + +config zone + option name 'lan' + option input 'ACCEPT' + option output 'ACCEPT' + option forward 'ACCEPT' + list network 'lan' + list network 'wg0' + +config zone + option name 'wan' + option input 'REJECT' + option output 'ACCEPT' + option forward 'REJECT' + option masq '1' + option mtu_fix '1' + list network 'wan' + list network 'wan6' + +config forwarding + option src 'lan' + option dest 'wan' + +config rule + option name 'Allow-DHCP-Renew' + option src 'wan' + option proto 'udp' + option dest_port '68' + option target 'ACCEPT' + option family 'ipv4' + +config rule + option name 'Allow-Ping' + option src 'wan' + option proto 'icmp' + option icmp_type 'echo-request' + option family 'ipv4' + option target 'ACCEPT' + +config rule + option name 'Allow-IGMP' + option src 'wan' + option proto 'igmp' + option family 'ipv4' + option target 'ACCEPT' + +config rule + option name 'Allow-DHCPv6' + option src 'wan' + option proto 'udp' + option dest_port '546' + option family 'ipv6' + option target 'ACCEPT' + +config rule + option name 'Allow-MLD' + option src 'wan' + option proto 'icmp' + option src_ip 'fe80::/10' + list icmp_type '130/0' + list icmp_type '131/0' + list icmp_type '132/0' + list icmp_type '143/0' + option family 'ipv6' + option target 'ACCEPT' + +config rule + option name 'Allow-ICMPv6-Input' + option src 'wan' + option proto 'icmp' + list icmp_type 'echo-request' + list icmp_type 'echo-reply' + list icmp_type 'destination-unreachable' + list icmp_type 'packet-too-big' + list icmp_type 'time-exceeded' + list icmp_type 'bad-header' + list icmp_type 'unknown-header-type' + list icmp_type 'router-solicitation' + list icmp_type 'neighbour-solicitation' + list icmp_type 'router-advertisement' + list icmp_type 'neighbour-advertisement' + option limit '1000/sec' + option family 'ipv6' + option target 'ACCEPT' + +config rule + option name 'Allow-ICMPv6-Forward' + option src 'wan' + option dest '*' + option proto 'icmp' + list icmp_type 'echo-request' + list icmp_type 'echo-reply' + list icmp_type 'destination-unreachable' + list icmp_type 'packet-too-big' + list icmp_type 'time-exceeded' + list icmp_type 'bad-header' + list icmp_type 'unknown-header-type' + option limit '1000/sec' + option family 'ipv6' + option target 'ACCEPT' + +config rule + option name 'Allow-IPSec-ESP' + option src 'wan' + option dest 'lan' + option proto 'esp' + option target 'ACCEPT' + +config rule + option name 'Allow-ISAKMP' + option src 'wan' + option dest 'lan' + option dest_port '500' + option proto 'udp' + option target 'ACCEPT' diff --git a/immortalwrt-patches/files/etc/config/network b/immortalwrt-patches/files/etc/config/network new file mode 100644 index 0000000..228219c --- /dev/null +++ b/immortalwrt-patches/files/etc/config/network @@ -0,0 +1,49 @@ +config interface 'loopback' + option device 'lo' + option proto 'static' + option ipaddr '127.0.0.1' + option netmask '255.0.0.0' + +config globals 'globals' + option ula_prefix 'fdab:c3fc:a55d::/48' + +config device + option name 'br-lan' + option type 'bridge' + list ports 'eth1' + +config interface 'lan' + option device 'br-lan' + option proto 'static' + option ipaddr '192.168.1.2' + option netmask '255.255.255.0' + option ip6assign '60' + +config interface 'wan' + option device 'eth0' + option proto 'dhcp' + +config interface 'wan6' + option device 'eth0' + option proto 'dhcpv6' + +config interface 'wg0' + option proto 'wireguard' + option private_key '/dev/null | grep -q 'set 1360' || \ + nft add rule inet fw4 mangle_forward 'iifname "br-lan" tcp flags syn tcp option maxseg size set 1360' 2>/dev/null +exit 0 diff --git a/immortalwrt-patches/files/etc/hotplug.d/iface/40-wg-track b/immortalwrt-patches/files/etc/hotplug.d/iface/40-wg-track new file mode 100644 index 0000000..6652bdf --- /dev/null +++ b/immortalwrt-patches/files/etc/hotplug.d/iface/40-wg-track @@ -0,0 +1,33 @@ +#!/bin/sh +# /etc/hotplug.d/iface/40-wg-track +# 触发: 小路由 WAN iface 起来时 (boot / DHCP renew / link bounce) +# 目的: 跟踪 WG 隧道自动恢复状态,验证坑 #15 是否真根除 +# 安装: scp 到 /etc/hotplug.d/iface/40-wg-track + chmod +x + +[ "$ACTION" = "ifup" ] || exit 0 +case "$INTERFACE" in + wan) ;; + *) exit 0 ;; +esac + +WAN_IP=$(ip -4 addr show eth0 2>/dev/null | awk '/inet / {print $2; exit}' | cut -d/ -f1) +[ -z "$WAN_IP" ] && exit 0 + +LOGFILE=/tmp/wg-tunnel-track.log + +# 等 30s 让 tunnel 稳定 (handshake init + keepalive 至少跑两轮) +sleep 30 + +TS=$(date '+%Y-%m-%d %H:%M:%S') +ENDPOINT=$(wg show wg0 2>/dev/null | awk '/endpoint:/ {print $2; exit}') +HANDSHAKE=$(wg show wg0 2>/dev/null | awk -F': ' '/latest handshake/ {print $2}') +TRANSFER=$(wg show wg0 2>/dev/null | awk '/transfer:/ {print $2, $3, $4, $5}') +PING_LOSS=$(ping -c 3 -W 2 10.99.0.1 2>&1 | awk '/packets transmitted/ {print $6}') + +STATUS="OK" +[ "$PING_LOSS" != "0%" ] && STATUS="FAIL" + +echo "$TS | status=$STATUS | WAN=$WAN_IP | endpoint=$ENDPOINT | handshake=$HANDSHAKE | loss=$PING_LOSS | xfer=$TRANSFER" >> "$LOGFILE" + +# Rotate: 只保留最后 200 行 +tail -200 "$LOGFILE" > "$LOGFILE.tmp" 2>/dev/null && mv "$LOGFILE.tmp" "$LOGFILE" diff --git a/immortalwrt-patches/files/etc/rc.local b/immortalwrt-patches/files/etc/rc.local new file mode 100644 index 0000000..be1fb92 --- /dev/null +++ b/immortalwrt-patches/files/etc/rc.local @@ -0,0 +1,26 @@ +#!/bin/sh +# RPS/RFS tuning for single-RX-queue devices (mtk_soc_eth) +# Distributes softirq across all CPUs; without this, all eth RX runs on CPU0 only. +# Pairs with sysctl tuning in /etc/sysctl.conf (BBR + buffer sizes). + +[ -d /sys/class/net/eth1/queues ] && echo 3 > /sys/class/net/eth1/queues/rx-0/rps_cpus +[ -d /sys/class/net/eth0/queues ] && echo 3 > /sys/class/net/eth0/queues/rx-0/rps_cpus +[ -d /sys/class/net/br-lan/queues ] && echo 3 > /sys/class/net/br-lan/queues/rx-0/rps_cpus +[ -d /sys/class/net/ra0/queues ] && echo 3 > /sys/class/net/ra0/queues/rx-0/rps_cpus +[ -d /sys/class/net/rax0/queues ] && echo 3 > /sys/class/net/rax0/queues/rx-0/rps_cpus + +echo 32768 > /proc/sys/net/core/rps_sock_flow_entries 2>/dev/null +[ -f /sys/class/net/eth1/queues/rx-0/rps_flow_cnt ] && echo 32768 > /sys/class/net/eth1/queues/rx-0/rps_flow_cnt +[ -f /sys/class/net/br-lan/queues/rx-0/rps_flow_cnt ] && echo 32768 > /sys/class/net/br-lan/queues/rx-0/rps_flow_cnt + +# === VXLAN boot fallback (keng #12 part 2) === +# Hotplug 20-vxlan sometimes misses wg0 ifup at boot (no 10.99 route, no log entry). +# If we got here without vxlan0, manually re-trigger the same script. +[ -e /sys/class/net/vxlan0 ] || ACTION=ifup INTERFACE=wg0 /etc/hotplug.d/net/20-vxlan + +# === WiFi band label verifier (v28, log-only) === +# Aligns UCI wifi labels with physical broadcast once driver is stable. +# Log-only (no uci mutation); see /etc/wifi-band-verify.sh for rationale. +( /etc/wifi-band-verify.sh >/dev/null 2>&1 & ) + +exit 0 diff --git a/immortalwrt-patches/files/etc/sysctl.d/99-bridge-nf.conf b/immortalwrt-patches/files/etc/sysctl.d/99-bridge-nf.conf new file mode 100644 index 0000000..316f344 --- /dev/null +++ b/immortalwrt-patches/files/etc/sysctl.d/99-bridge-nf.conf @@ -0,0 +1,3 @@ +net.bridge.bridge-nf-call-ip6tables=1 +net.bridge.bridge-nf-call-iptables=1 +net.bridge.bridge-nf-call-arptables=1 diff --git a/immortalwrt-patches/files/etc/uci-defaults/99-custom-config b/immortalwrt-patches/files/etc/uci-defaults/99-custom-config new file mode 100644 index 0000000..1e6dd7d --- /dev/null +++ b/immortalwrt-patches/files/etc/uci-defaults/99-custom-config @@ -0,0 +1,66 @@ +#!/bin/sh +# 99-custom-config - Apply TR3000-specific defaults on first boot +# Sets LAN IP, WiFi SSID/htmode/uapsd, dnsmasq, IPv6 RA, turboacc BBR +# Runs once (uci-defaults scripts auto-delete after running) +# +# v29 2026-07-22: HONEST labels. UCI idx=1=5g config (driver maps +# band=5g content to the 5G PHY whatever netdev it is this boot). +# idx=2=2g config (maps to 2.4G PHY). ra0/rax0 netdev binding is +# random per boot but physical broadcast always correct (band-content +# invariant). LuCI Overview shows iwinfo truth. Edit forms show UCI +# config. Do NOT use LuCI Save and Apply (triggers wifi reload); +# reboot to apply wireless changes. + +. /lib/functions.sh + +# ===== LAN IP ===== +uci set network.lan.ipaddr="192.168.1.2" +uci set network.lan.netmask="255.255.255.0" +uci commit network + +# ===== WiFi SSID + htmode + uapsd (honest labels) ===== +uci set wireless.MT7981_1_1.phy="ra0" +uci set wireless.MT7981_1_1.band="5g" +uci set wireless.MT7981_1_1.hwmode="11a" +uci set wireless.MT7981_1_1.htmode="HE160" +uci set wireless.MT7981_1_1.country="CN" +uci set wireless.MT7981_1_1.channel="36" +uci set wireless.MT7981_1_1.dbdc_main="1" +uci set wireless.default_MT7981_1_1.ssid="ImmortalWrt-5G" +uci set wireless.default_MT7981_1_1.uapsd="0" +uci set wireless.default_MT7981_1_1.encryption="none" + +uci set wireless.MT7981_1_2.phy="rax0" +uci set wireless.MT7981_1_2.band="2g" +uci set wireless.MT7981_1_2.hwmode="11g" +uci set wireless.MT7981_1_2.htmode="HE40" +uci set wireless.MT7981_1_2.country="CN" +uci set wireless.MT7981_1_2.channel="auto" +uci set wireless.MT7981_1_2.dbdc_main="0" +uci set wireless.default_MT7981_1_2.ssid="ImmortalWrt-2.4G" +uci set wireless.default_MT7981_1_2.uapsd="0" +uci set wireless.default_MT7981_1_2.encryption="none" + +uci commit wireless + +# ===== dnsmasq enable ===== +/etc/init.d/dnsmasq enable + +# ===== IPv6 RA disabled ===== +uci set dhcp.lan.ra="disabled" +uci set dhcp.lan.dhcpv6="disabled" +uci commit dhcp + +# ===== turboacc: BBR as default TCP CCA ===== +# /etc/init.d/turboacc force-sets tcp_congestion_control from +# turboacc.config.tcpcca at every boot (S90, after sysctl.d), defaulting +# to cubic and clobbering sysctl.d/12-tcp-bbr.conf. Pin bbr here so the +# 网络加速 page shows BBR active out of the box. +uci set turboacc.config.tcpcca="bbr" +uci commit turboacc + +# ===== Firewall: prepare for MSS clamp via hotplug ===== +# fw4 reload wipes hotplug MSS clamp rules; the 30-mss-clamp hotplug +# re-adds them on every iface ifup. No static config needed here. + +exit 0 diff --git a/immortalwrt-patches/files/etc/wifi-band-verify.sh b/immortalwrt-patches/files/etc/wifi-band-verify.sh new file mode 100644 index 0000000..9d1c7c1 --- /dev/null +++ b/immortalwrt-patches/files/etc/wifi-band-verify.sh @@ -0,0 +1,59 @@ +#!/bin/sh +# wifi-band-verify.sh — LOG-ONLY diagnostic. Compares UCI labels against the +# PHYSICAL band each netdev is on and records the verdict to logread. +# NEVER writes UCI: with ra0/rax0 ↔ dat binding random per boot (and band-content +# invariant keeping physical always correct), mutating UCI oscillates across +# wifi reloads. Logging the mismatch is enough — LuCI display patch (wireless.js +# getActiveSSID) makes the UI match iwinfo truth regardless of UCI labels. +# Triggered by rc.local at boot and mtwifi_cfg dispatcher tail. +# Commit-only would still be loop-safe, but useless: detection is non-deterministic +# across reloads, so any commit causes oscillation. Log-only is structurally stable. + +LOCK=/tmp/wifi-band-verify.lock +NOW=$(date +%s) +if [ -f "$LOCK" ]; then + LAST=$(cat "$LOCK" 2>/dev/null) + [ -n "$LAST" ] && [ $((NOW - LAST)) -lt 120 ] && exit 0 +fi +echo "$NOW" > "$LOCK" + +log() { logger -t wifi-band-verify -- "$*"; } + +get_ch() { iwinfo "$1" info 2>/dev/null | awk '/Channel:/ {print $4}'; } +get_essid(){ iwinfo "$1" info 2>/dev/null | awk -F"\x22" '/ESSID:/ {print $2}'; } + +# Phase 1: wait until both radios report identical channels on two consecutive +# 10s probes (guards against mid-init transient reads; max 18 rounds = 3min). +ra0ch=""; rax0ch=""; stable=0; i=0 +while [ $i -lt 18 ]; do + c1=$(get_ch ra0); c2=$(get_ch rax0) + case "$c1" in ""|*[!0-9]*) c1="" ;; esac + case "$c2" in ""|*[!0-9]*) c2="" ;; esac + if [ -n "$c1" ] && [ -n "$c2" ] && [ "$c1" = "$ra0ch" ] && [ "$c2" = "$rax0ch" ]; then + stable=1; break + fi + ra0ch=$c1; rax0ch=$c2 + i=$((i+1)); sleep 10 +done +[ $stable -eq 1 ] || { log "radios not stable after 3min; skipped"; exit 0; } + +# Phase 2: physical band from channel (2g ≤14, 5g ≥36) +band_of() { [ "$1" -le 14 ] && echo 2g || echo 5g; } +ra0band=$(band_of "$ra0ch"); rax0band=$(band_of "$rax0ch") +[ "$ra0band" = "$rax0band" ] && { log "both radios on $ra0band ch$ra0ch/$rax0ch; abnormal"; exit 0; } + +# Phase 3: read UCI labels and live ESSIDs, log verdict — NO mutation. +uci -q get wireless.MT7981_1_1 >/dev/null || exit 0 +uci -q get wireless.MT7981_1_2 >/dev/null || exit 0 +u1=$(uci -q get wireless.MT7981_1_1.band) +u2=$(uci -q get wireless.MT7981_1_2.band) +s1=$(uci -q get wireless.default_MT7981_1_1.ssid) +s2=$(uci -q get wireless.default_MT7981_1_2.ssid) +e1=$(get_essid ra0); e2=$(get_essid rax0) + +if [ "$u1" = "$ra0band" ] && [ "$u2" = "$rax0band" ] && [ "$s1" = "$e1" ] && [ "$s2" = "$e2" ]; then + log "consistent: ra0=$ra0band ch$ra0ch [$e1], rax0=$rax0band ch$rax0ch [$e2]; uci=$u1/$u2" +else + log "MISMATCH (no-op): ra0=$ra0band ch$ra0ch [$e1], rax0=$rax0band ch$rax0ch [$e2]; uci=$u1/$u2 ssid=$s1/$s2" +fi +exit 0 diff --git a/immortalwrt-patches/netifd-wireless.sh b/immortalwrt-patches/netifd-wireless.sh new file mode 100644 index 0000000..c3772bb --- /dev/null +++ b/immortalwrt-patches/netifd-wireless.sh @@ -0,0 +1,440 @@ +NETIFD_MAIN_DIR="${NETIFD_MAIN_DIR:-/lib/netifd}" + +. /usr/share/libubox/jshn.sh +. $NETIFD_MAIN_DIR/utils.sh + +CMD_UP=0 +CMD_SET_DATA=1 +CMD_PROCESS_ADD=2 +CMD_PROCESS_KILL_ALL=3 +CMD_SET_RETRY=4 + +add_driver() { + return +} + +wireless_setup_vif_failed() { + local error="$1" + echo "Interface $_w_iface setup failed: $error" +} + +wireless_setup_failed() { + local error="$1" + + echo "Device setup failed: $error" + wireless_set_retry 0 +} + +prepare_key_wep() { + local key="$1" + local hex=1 + + echo -n "$key" | grep -qE "[^a-fA-F0-9]" && hex=0 + [ "${#key}" -eq 10 -a $hex -eq 1 ] || \ + [ "${#key}" -eq 26 -a $hex -eq 1 ] || { + [ "${key:0:2}" = "s:" ] && key="${key#s:}" + key="$(echo -n "$key" | hexdump -ve '1/1 "%02x" ""')" + } + echo "$key" +} + +_wdev_prepare_channel() { + json_get_vars channel band hwmode + + auto_channel=0 + enable_ht=0 + htmode= + hwmode="${hwmode##11}" + + case "$channel" in + ""|0|auto) + channel=0 + auto_channel=1 + ;; + [0-9]*) ;; + *) + wireless_setup_failed "INVALID_CHANNEL" + ;; + esac + + case "$hwmode" in + a|b|g|ad) ;; + *) + if [ "$channel" -gt 14 ]; then + hwmode=a + else + hwmode=g + fi + ;; + esac + + case "$band" in + 2g) hwmode=g;; + 5g|6g) hwmode=a;; + 60g) hwmode=ad;; + *) + case "$hwmode" in + *a) band=5g;; + *ad) band=60g;; + *b|*g) band=2g;; + esac + ;; + esac +} + +_wdev_handler() { + json_load "$data" + + json_select config + _wdev_prepare_channel + json_select .. + + eval "drv_$1_$2 \"$interface\"" +} + +_wdev_msg_call() { + local old_cb + + json_set_namespace wdev old_cb + "$@" + json_set_namespace $old_cb +} + +_wdev_wrapper() { + while [ -n "$1" ]; do + eval "$1() { _wdev_msg_call _$1 \"\$@\"; }" + shift + done +} + +_wdev_notify_init() { + local command="$1"; shift; + + json_init + json_add_int "command" "$command" + json_add_string "device" "$__netifd_device" + while [ -n "$1" ]; do + local name="$1"; shift + local value="$1"; shift + json_add_string "$name" "$value" + done + json_add_object "data" +} + +_wdev_notify() { + local options="$1" + + json_close_object + ubus $options call network.wireless notify "$(json_dump)" +} + +_wdev_add_variables() { + while [ -n "$1" ]; do + local var="${1%%=*}" + local val="$1" + shift + [[ "$var" = "$val" ]] && continue + val="${val#*=}" + json_add_string "$var" "$val" + done +} + +_wireless_add_vif() { + local name="$1"; shift + local ifname="$1"; shift + + _wdev_notify_init $CMD_SET_DATA "interface" "$name" + json_add_string "ifname" "$ifname" + _wdev_add_variables "$@" + _wdev_notify +} + +_wireless_add_vlan() { + local name="$1"; shift + local ifname="$1"; shift + + _wdev_notify_init $CMD_SET_DATA interface "$__cur_interface" "vlan" "$name" + json_add_string "ifname" "$ifname" + _wdev_add_variables "$@" + _wdev_notify +} + +_wireless_set_up() { + _wdev_notify_init $CMD_UP + _wdev_notify +} + +_wireless_set_data() { + _wdev_notify_init $CMD_SET_DATA + _wdev_add_variables "$@" + _wdev_notify +} + +_wireless_add_process() { + _wdev_notify_init $CMD_PROCESS_ADD + local exe="$2" + [ -L "$exe" ] && exe="$(readlink -f "$exe")" + json_add_int pid "$1" + json_add_string exe "$exe" + [ -n "$3" ] && json_add_boolean required 1 + [ -n "$4" ] && json_add_boolean keep 1 + exe2="$(readlink -f /proc/$1/exe)" + [ "$exe" != "$exe2" ] && echo "WARNING (wireless_add_process): executable path $exe does not match process $1 path ($exe2)" + _wdev_notify +} + +_wireless_process_kill_all() { + _wdev_notify_init $CMD_PROCESS_KILL_ALL + [ -n "$1" ] && json_add_int signal "$1" + _wdev_notify +} + +_wireless_set_retry() { + _wdev_notify_init $CMD_SET_RETRY + json_add_int retry "$1" + _wdev_notify +} + +_wdev_wrapper \ + wireless_add_vif \ + wireless_add_vlan \ + wireless_set_up \ + wireless_set_data \ + wireless_add_process \ + wireless_process_kill_all \ + wireless_set_retry \ + +wireless_vif_parse_encryption() { + json_get_vars encryption + set_default encryption none + + auth_mode_open=1 + auth_mode_shared=0 + auth_type=none + + if [ "$hwmode" = "ad" ]; then + wpa_cipher="GCMP" + else + wpa_cipher="CCMP" + fi + + case "$encryption" in + *tkip+aes|*tkip+ccmp|*aes+tkip|*ccmp+tkip) wpa_cipher="CCMP TKIP";; + *ccmp256) wpa_cipher="CCMP-256";; + *aes|*ccmp) wpa_cipher="CCMP";; + *tkip) wpa_cipher="TKIP";; + *gcmp256) wpa_cipher="GCMP-256";; + *gcmp) wpa_cipher="GCMP";; + wpa3-192*) wpa_cipher="GCMP-256";; + esac + + # 802.11n requires CCMP for WPA + [ "$enable_ht:$wpa_cipher" = "1:TKIP" ] && wpa_cipher="CCMP TKIP" + + # Examples: + # psk-mixed/tkip => WPA1+2 PSK, TKIP + # wpa-psk2/tkip+aes => WPA2 PSK, CCMP+TKIP + # wpa2/tkip+aes => WPA2 RADIUS, CCMP+TKIP + + case "$encryption" in + wpa2*|wpa3*|*psk2*|psk3*|sae*|owe*) + wpa=2 + ;; + wpa*mixed*|*psk*mixed*) + wpa=3 + ;; + wpa*|*psk*) + wpa=1 + ;; + *) + wpa=0 + wpa_cipher= + ;; + esac + wpa_pairwise="$wpa_cipher" + + case "$encryption" in + owe*) + auth_type=owe + ;; + wpa3-192*) + auth_type=eap192 + ;; + wpa3-mixed*) + auth_type=eap-eap2 + ;; + wpa3*) + auth_type=eap2 + ;; + psk3-mixed*|sae-mixed*) + auth_type=psk-sae + ;; + psk3*|sae*) + auth_type=sae + ;; + *psk*) + auth_type=psk + ;; + *wpa*|*8021x*) + auth_type=eap + ;; + *wep*) + auth_type=wep + case "$encryption" in + *shared*) + auth_mode_open=0 + auth_mode_shared=1 + ;; + *mixed*) + auth_mode_shared=1 + ;; + esac + ;; + esac + + case "$encryption" in + *osen*) + auth_osen=1 + ;; + esac +} + +_wireless_set_brsnoop_isolation() { + local multicast_to_unicast="$1" + local isolate + + json_get_vars isolate proxy_arp + + [ ${isolate:-0} -gt 0 -o -z "$network_bridge" ] && return + [ ${multicast_to_unicast:-1} -gt 0 -o ${proxy_arp:-0} -gt 0 ] && json_add_boolean isolate 1 +} + +for_each_interface() { + local _w_types="$1"; shift + local _w_ifaces _w_iface + local _w_type + local _w_found + + local multicast_to_unicast + + json_get_keys _w_ifaces interfaces + json_select interfaces + for _w_iface in $_w_ifaces; do + json_select "$_w_iface" + if [ -n "$_w_types" ]; then + json_get_var network_bridge bridge + json_get_var network_ifname bridge-ifname + json_get_var multicast_to_unicast multicast_to_unicast + json_select config + _wireless_set_brsnoop_isolation "$multicast_to_unicast" + json_get_var _w_type mode + json_select .. + _w_types=" $_w_types " + [[ "${_w_types%$_w_type*}" = "$_w_types" ]] && { + json_select .. + continue + } + fi + __cur_interface="$_w_iface" + "$@" "$_w_iface" + json_select .. + done + json_select .. +} + +for_each_vlan() { + local _w_vlans _w_vlan + + json_get_keys _w_vlans vlans + json_select vlans + for _w_vlan in $_w_vlans; do + json_select "$_w_vlan" + json_select config + "$@" "$_w_vlan" + json_select .. + json_select .. + done + json_select .. +} + +for_each_station() { + local _w_stas _w_sta + + json_get_keys _w_stas stas + json_select stas + for _w_sta in $_w_stas; do + json_select "$_w_sta" + json_select config + "$@" "$_w_sta" + json_select .. + json_select .. + done + json_select .. +} + +_wdev_common_device_config() { + config_add_string channel hwmode band htmode noscan +} + +_wdev_common_iface_config() { + config_add_string mode ssid encryption 'key:wpakey' + config_add_boolean bridge_isolate + config_add_array tags +} + +_wdev_common_vlan_config() { + config_add_string name vid iface + config_add_boolean bridge_isolate +} + +_wdev_common_station_config() { + config_add_string mac key vid iface +} + +init_wireless_driver() { + name="$1"; shift + cmd="$1"; shift + + case "$cmd" in + dump) + add_driver() { + eval "drv_$1_cleanup" + + json_init + json_add_string name "$1" + + json_add_array device + _wdev_common_device_config + eval "drv_$1_init_device_config" + json_close_array + + json_add_array iface + _wdev_common_iface_config + eval "drv_$1_init_iface_config" + json_close_array + + json_add_array vlan + _wdev_common_vlan_config + eval "drv_$1_init_vlan_config" + json_close_array + + json_add_array station + _wdev_common_station_config + eval "drv_$1_init_station_config" + json_close_array + + json_dump + } + ;; + setup|teardown) + interface="$1"; shift + data="$1"; shift + export __netifd_device="$interface" + + add_driver() { + [[ "$name" == "$1" ]] || return 0 + _wdev_handler "$1" "$cmd" + } + ;; + esac +} diff --git a/immortalwrt-patches/package/mtk/applications/luci-app-mtk/luasrc/controller/mtkwifi.lua b/immortalwrt-patches/package/mtk/applications/luci-app-mtk/luasrc/controller/mtkwifi.lua new file mode 100644 index 0000000..e2fc13a --- /dev/null +++ b/immortalwrt-patches/package/mtk/applications/luci-app-mtk/luasrc/controller/mtkwifi.lua @@ -0,0 +1,1380 @@ +module("luci.controller.mtkwifi", package.seeall) + +local ioctl_help = require "ioctl_helper" +local http = require("luci.http") +local i18n = require("luci.i18n") +local mtkwifi = require("mtkwifi") +local sys = require "luci.sys" +local jsc = require "luci.jsonc" +local nfs = require "nixio.fs" + +local logDisable = 0 +function debug_write(...) + -- luci.http.write(...) + if logDisable == 1 then + return + end + local syslog_msg = ""; + local nargs = select('#',...) + + for n=1, nargs do + local v = select(n,...) + if (type(v) == "string" or type(v) == "number") then + syslog_msg = syslog_msg..v.." "; + elseif (type(v) == "boolean") then + if v then + syslog_msg = syslog_msg.."true "; + else + syslog_msg = syslog_msg.."false "; + end + elseif (type(v) == "nil") then + syslog_msg = syslog_msg.."nil "; + else + syslog_msg = syslog_msg.." "; + end + end + nixio.syslog("debug", syslog_msg) +end + +function index() + -- v17.1 2026-07-20: MTK wireless panel disabled by claude.md. + -- Empty body so dispatcher registers nothing under admin/network/wifi. + -- See project_luci_dual_wireless_menu.md. + return +end + +function test() + http.write_json(http.formvalue()) +end + +function exec_reboot() + os.execute("rm -f /tmp/mtk/wifi/reboot_required >/dev/null 2>&1") + os.execute("sync >/dev/null 2>&1") + os.execute("reboot >/dev/null 2>&1") +end + +function get_apply_status() + local ret = {} + + if mtkwifi.is_child_active() then + ret["status"] = "ON_PROGRESS" + elseif mtkwifi.exists("/tmp/mtk/wifi/reboot_required") then + -- If the "wifi restart" command can not re-install the driver; then, it will create + -- "/tmp/mtk/wifi/reboot_required" file to indicate LuCI that the settings will be applied + -- only after reboot of the device. + -- Redirect "Reboot Device" web-page to get consent from the user to reboot the device. + ret["status"] = "REBOOT" + else + ret["status"] = "DONE" + end + http.write_json(ret) +end + +function __mtkwifi_save_profile(cfgs, path, isProfileSettingsAppliedToDriver) + -- Create the applied settings backup file before saving the new profile settings only if it does not exist. + if not mtkwifi.exists(mtkwifi.__profile_applied_settings_path(path)) then + os.execute("cp -f "..path.." "..mtkwifi.__profile_applied_settings_path(path)) + end + if isProfileSettingsAppliedToDriver then + -- It means the some context based profile settings to be saved in DAT file is already applied to the driver. + -- Find the profile settings which are not applied to the driver before saving the new profile settings + local diff = mtkwifi.diff_profile(path) + mtkwifi.save_profile(cfgs, path) + -- If there are any settings which are not applied to the driver, then do NOT copy and WebUI will display the "need reload to apply changes" message + -- Otherwise, copy the new profile settings and WebUI will NOT display the "need reload to apply changes" message + if next(diff) == nil then + os.execute("cp -f "..path.." "..mtkwifi.__profile_applied_settings_path(path)) + end + else + mtkwifi.save_profile(cfgs, path) + end +end + +local __get_default_wan_ifname = function() + local boardinfo = jsc.parse(nfs.readfile("/etc/board.json") or "") + local wan_ifname = "eth1" + + if type(boardinfo) == "table" and type(boardinfo.network.wan.device) == "string" then + wan_ifname = boardinfo.network.wan.device + end + + return wan_ifname +end + +local __setup_wan = function(devname) + local devs = mtkwifi.get_all_devs() + local profiles = mtkwifi.search_dev_and_profile() + local cfgs = mtkwifi.load_profile(profiles[devname]) + local dev = devs and devs[devname] + local vif = __get_default_wan_ifname() + + if cfgs.ApCliEnable ~= "0" and cfgs.ApCliEnable ~= "" and dev.apcli.vifname then + vif = dev.apcli.vifname + end + + nixio.syslog("debug", "Set wan/wan6 to "..vif) + + os.execute("uci set network.wan.device="..vif) + os.execute("uci set network.wan6.device="..vif) + os.execute("uci commit") + os.execute("ifup wan") + os.execute("ifup wan6") + +end + +local __mtkwifi_reload = function (devname) + local wifi_restart = false + local wifi_reload = false + local change_wan = false + local profiles = mtkwifi.search_dev_and_profile() + + for dev,profile in pairs(profiles) do + if not devname or devname == dev then + local diff = mtkwifi.diff_profile(profile) + __process_settings_before_apply(dev, profile, diff) + + if diff.BssidNum or diff.WHNAT or diff.E2pAccessMode or diff.HT_RxStream or diff.HT_TxStream or diff.WdsEnable then + -- Addition or deletion of a vif requires re-installation of the driver. + -- Change in WHNAT setting also requires re-installation of the driver. + -- Driver will be re-installed by "wifi restart" command. + wifi_restart = true + else + wifi_reload = true + end + + if diff.ApCliEnable then + change_wan = true + end + end + end + + if change_wan and devname then + __setup_wan(devname) + end + + if wifi_restart then + os.execute("wifi restart "..(devname or "")) + debug_write("wifi restart "..(devname or "")) + elseif wifi_reload then + os.execute("wifi reload "..(devname or "")) + debug_write("wifi reload "..(devname or "")) + end + + for dev,profile in pairs(profiles) do + if not devname or devname == dev then + -- keep a backup for this commit + -- it will be used in mtkwifi.diff_profile() + os.execute("cp -f "..profile.." "..mtkwifi.__profile_applied_settings_path(profile)) + debug_write("cp -f "..profile.." "..mtkwifi.__profile_applied_settings_path(profile)) + end + end + +end + +function __process_settings_before_apply(devname, profile, diff) + local devs = mtkwifi.get_all_devs() + local cfgs = mtkwifi.load_profile(profile) + __apply_wifi_wpsconf(devs, devname, cfgs, diff) +end + +function chip_cfg(devname) + local profiles = mtkwifi.search_dev_and_profile() + assert(profiles[devname]) + local cfgs = mtkwifi.load_profile(profiles[devname]) + local devs = mtkwifi.get_all_devs() + local dbdc_cfgs = {} + local dev = {} + dev = devs and devs[devname] + + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + nixio.syslog("err", "chip_cfg, invalid value type for "..k..","..type(v)) + elseif string.byte(k) == string.byte("_") then + nixio.syslog("err", "chip_cfg, special: "..k.."="..v) + else + if dev.dbdc == true then + dbdc_cfgs[k] = v or "" + else + cfgs[k] = v or "" + end + end + end + + -- VOW + -- ATC should actually be scattered into each SSID, but I'm just lazy. + if cfgs.VOW_Airtime_Fairness_En then + for i = 1,tonumber(cfgs.BssidNum) do + __atc_tp = http.formvalue("__atc_vif"..i.."_tp") or "0" + __atc_min_tp = http.formvalue("__atc_vif"..i.."_min_tp") or "0" + __atc_max_tp = http.formvalue("__atc_vif"..i.."_max_tp") or "0" + __atc_at = http.formvalue("__atc_vif"..i.."_at") or "0" + __atc_min_at = http.formvalue("__atc_vif"..i.."_min_at") or "0" + __atc_max_at = http.formvalue("__atc_vif"..i.."_max_at") or "0" + + nixio.syslog("info", "ATC.__atc_tp ="..i..__atc_tp ); + nixio.syslog("info", "ATC.__atc_min_tp ="..i..__atc_min_tp ); + nixio.syslog("info", "ATC.__atc_max_tp ="..i..__atc_max_tp ); + nixio.syslog("info", "ATC.__atc_at ="..i..__atc_at ); + nixio.syslog("info", "ATC.__atc_min_at ="..i..__atc_min_at ); + nixio.syslog("info", "ATC.__atc_max_at ="..i..__atc_max_at ); + + dbdc_cfgs.VOW_Rate_Ctrl_En = mtkwifi.token_set(cfgs.VOW_Rate_Ctrl_En, i, __atc_tp) + dbdc_cfgs.VOW_Group_Min_Rate = mtkwifi.token_set(cfgs.VOW_Group_Min_Rate, i, __atc_min_tp) + dbdc_cfgs.VOW_Group_Max_Rate = mtkwifi.token_set(cfgs.VOW_Group_Max_Rate, i, __atc_max_tp) + + dbdc_cfgs.VOW_Airtime_Ctrl_En = mtkwifi.token_set(cfgs.VOW_Airtime_Ctrl_En, i, __atc_at) + dbdc_cfgs.VOW_Group_Min_Ratio = mtkwifi.token_set(cfgs.VOW_Group_Min_Ratio, i, __atc_min_at) + dbdc_cfgs.VOW_Group_Max_Ratiio = mtkwifi.token_set(cfgs.VOW_Group_Max_Ratio, i, __atc_max_at) + + cfgs.VOW_Rate_Ctrl_En = mtkwifi.token_set(cfgs.VOW_Rate_Ctrl_En, i, __atc_tp) + cfgs.VOW_Group_Min_Rate = mtkwifi.token_set(cfgs.VOW_Group_Min_Rate, i, __atc_min_tp) + cfgs.VOW_Group_Max_Rate = mtkwifi.token_set(cfgs.VOW_Group_Max_Rate, i, __atc_max_tp) + + cfgs.VOW_Airtime_Ctrl_En = mtkwifi.token_set(cfgs.VOW_Airtime_Ctrl_En, i, __atc_at) + cfgs.VOW_Group_Min_Ratio = mtkwifi.token_set(cfgs.VOW_Group_Min_Ratio, i, __atc_min_at) + cfgs.VOW_Group_Max_Ratio = mtkwifi.token_set(cfgs.VOW_Group_Max_Ratio, i, __atc_max_at) + + end + + dbdc_cfgs.VOW_RX_En = http.formvalue("VOW_RX_En") or "0" + cfgs.VOW_RX_En = http.formvalue("VOW_RX_En") or "0" + end + + if dev.dbdc == true then + for devname, profile in pairs(profiles) do + __mtkwifi_save_profile(dbdc_cfgs, profile, false) + end + else + __mtkwifi_save_profile(cfgs, profiles[devname], false) + end + + if http.formvalue("__apply") then + mtkwifi.__run_in_child_env(__mtkwifi_reload, devname) + local url_to_visit_after_reload = luci.dispatcher.build_url("admin", "network", "wifi", "chip_cfg_view",devname) + luci.http.redirect(luci.dispatcher.build_url("admin", "network", "wifi", "loading",url_to_visit_after_reload)) + else + luci.http.redirect(luci.dispatcher.build_url("admin", "network", "wifi", "chip_cfg_view",devname)) + end + +end + +function dev_cfg(devname) + local profiles = mtkwifi.search_dev_and_profile() + assert(profiles[devname]) + local cfgs = mtkwifi.load_profile(profiles[devname]) + + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + nixio.syslog("err", "dev_cfg, invalid value type for "..k..","..type(v)) + elseif string.byte(k) == string.byte("_") then + nixio.syslog("err", "dev_cfg, special: "..k.."="..v) + else + cfgs[k] = v or "" + end + end + + if cfgs.Channel == "0" then -- Auto Channel Select + cfgs.AutoChannelSelect = "3" + else + cfgs.AutoChannelSelect = "0" + end + + if http.formvalue("__bw") == "20" then + cfgs.HT_BW = 0 + cfgs.VHT_BW = 0 + elseif http.formvalue("__bw") == "40" then + cfgs.HT_BW = 1 + cfgs.VHT_BW = 0 + cfgs.HT_BSSCoexistence = 0 + elseif http.formvalue("__bw") == "60" then + cfgs.HT_BW = 1 + cfgs.VHT_BW = 0 + cfgs.HT_BSSCoexistence = 1 + elseif http.formvalue("__bw") == "80" then + cfgs.HT_BW = 1 + cfgs.VHT_BW = 1 + elseif http.formvalue("__bw") == "160" then + cfgs.HT_BW = 1 + cfgs.VHT_BW = 2 + elseif http.formvalue("__bw") == "161" then + cfgs.HT_BW = 1 + cfgs.VHT_BW = 3 + cfgs.VHT_Sec80_Channel = http.formvalue("VHT_Sec80_Channel") or "" + end + + if mtkwifi.band(string.split(cfgs.WirelessMode,";")[1]) == "5G" or mtkwifi.band(cfgs.WirelessMode) == "6G" then + cfgs.CountryRegionABand = http.formvalue("__cr"); + else + cfgs.CountryRegion = http.formvalue("__cr"); + end + + if http.formvalue("TxPower") then + local txpower = tonumber(http.formvalue("TxPower")) + if txpower < 100 then + cfgs.PERCENTAGEenable=1 + else + cfgs.PERCENTAGEenable=0 + end + end + + local mimo = http.formvalue("__mimo") + if mimo == "0" then + cfgs.ETxBfEnCond=1 + cfgs.MUTxRxEnable=0 + cfgs.ITxBfEn=0 + elseif mimo == "1" then + cfgs.ETxBfEnCond=0 + cfgs.MUTxRxEnable=0 + cfgs.ITxBfEn=1 + elseif mimo == "2" then + cfgs.ETxBfEnCond=1 + cfgs.MUTxRxEnable=0 + cfgs.ITxBfEn=1 + elseif mimo == "3" then + cfgs.ETxBfEnCond=1 + if tonumber(cfgs.ApCliEnable) == 1 then + cfgs.MUTxRxEnable=3 + else + cfgs.MUTxRxEnable=1 + end + cfgs.ITxBfEn=0 + elseif mimo == "4" then + cfgs.ETxBfEnCond=1 + if tonumber(cfgs.ApCliEnable) == 1 then + cfgs.MUTxRxEnable=3 + else + cfgs.MUTxRxEnable=1 + end + cfgs.ITxBfEn=1 + else + cfgs.ETxBfEnCond=0 + cfgs.MUTxRxEnable=0 + cfgs.ITxBfEn=0 + end + +-- if cfgs.ApCliEnable == "1" then +-- cfgs.Channel = http.formvalue("__apcli_channel") +-- end + + -- WDS + -- http.write_json(http.formvalue()) + __mtkwifi_save_profile(cfgs, profiles[devname], false) + + if http.formvalue("__apply") then + mtkwifi.__run_in_child_env(__mtkwifi_reload, devname) + local url_to_visit_after_reload = luci.dispatcher.build_url("admin", "network", "wifi", "dev_cfg_view",devname) + luci.http.redirect(luci.dispatcher.build_url("admin", "network", "wifi", "loading",url_to_visit_after_reload)) + else + luci.http.redirect(luci.dispatcher.build_url("admin", "network", "wifi", "dev_cfg_view",devname)) + end +end + +function dev_cfg_raw(devname) + -- http.write_json(http.formvalue()) + local profiles = mtkwifi.search_dev_and_profile() + assert(profiles[devname]) + + local raw = http.formvalue("raw") + raw = string.gsub(raw, "\r\n", "\n") + local cfgs = mtkwifi.load_profile(nil, raw) + __mtkwifi_save_profile(cfgs, profiles[devname], false) + + luci.http.redirect(luci.dispatcher.build_url("admin", "network", "wifi", "dev_cfg_view", devname)) +end + +function __delete_mbss_para(cfgs, vif_idx) + debug_write("delete_mbss_para vif: ", vif_idx) + cfgs["WPAPSK"..vif_idx]="" + cfgs["Key1Type"]=mtkwifi.token_set(cfgs["Key1Type"],vif_idx,"") + cfgs["Key2Type"]=mtkwifi.token_set(cfgs["Key2Type"],vif_idx,"") + cfgs["Key3Type"]=mtkwifi.token_set(cfgs["Key3Type"],vif_idx,"") + cfgs["Key4Type"]=mtkwifi.token_set(cfgs["Key4Type"],vif_idx,"") + cfgs["RADIUS_Server"]=mtkwifi.token_set(cfgs["RADIUS_Server"],vif_idx,"") + cfgs["RADIUS_Port"]=mtkwifi.token_set(cfgs["RADIUS_Port"],vif_idx,"") + cfgs["RADIUS_Key"..vif_idx]="" + cfgs["DefaultKeyID"]=mtkwifi.token_set(cfgs["DefaultKeyID"],vif_idx,"") + cfgs["IEEE8021X"]=mtkwifi.token_set(cfgs["IEEE8021X"],vif_idx,"") + cfgs["WscConfMode"]=mtkwifi.token_set(cfgs["WscConfMode"],vif_idx,"") + cfgs["PreAuth"]=mtkwifi.token_set(cfgs["PreAuth"],vif_idx,"") + cfgs["HT_STBC"] = mtkwifi.token_set(cfgs["HT_STBC"],vif_idx,"") + cfgs["HT_LDPC"] = mtkwifi.token_set(cfgs["HT_LDPC"],vif_idx,"") + cfgs["VHT_STBC"] = mtkwifi.token_set(cfgs["VHT_STBC"],vif_idx,"") + cfgs["VHT_LDPC"] = mtkwifi.token_set(cfgs["VHT_LDPC"],vif_idx,"") + cfgs["HideSSID"]=mtkwifi.token_set(cfgs["HideSSID"],vif_idx,"") + cfgs["NoForwarding"]=mtkwifi.token_set(cfgs["NoForwarding"],vif_idx,"") + cfgs["WmmCapable"]=mtkwifi.token_set(cfgs["WmmCapable"],vif_idx,"") + cfgs["TxRate"]=mtkwifi.token_set(cfgs["TxRate"],vif_idx,"") + cfgs["RekeyInterval"]=mtkwifi.token_set(cfgs["RekeyInterval"],vif_idx,"") + cfgs["AuthMode"]=mtkwifi.token_set(cfgs["AuthMode"],vif_idx,"") + cfgs["EncrypType"]=mtkwifi.token_set(cfgs["EncrypType"],vif_idx,"") + cfgs["session_timeout_interval"]=mtkwifi.token_set(cfgs["session_timeout_interval"],vif_idx,"") + --cfgs["WscModeOption"]=mtkwifi.token_set(cfgs["WscModeOption"],vif_idx,"") + cfgs["RekeyMethod"]=mtkwifi.token_set(cfgs["RekeyMethod"],vif_idx,"") + cfgs["PMFMFPC"] = mtkwifi.token_set(cfgs["PMFMFPC"],vif_idx,"") + cfgs["PMFMFPR"] = mtkwifi.token_set(cfgs["PMFMFPR"],vif_idx,"") + cfgs["PMFSHA256"] = mtkwifi.token_set(cfgs["PMFSHA256"],vif_idx,"") + cfgs["PMKCachePeriod"] = mtkwifi.token_set(cfgs["PMKCachePeriod"],vif_idx,"") + cfgs["Wapiifname"] = mtkwifi.token_set(cfgs["Wapiifname"],vif_idx,"") + cfgs["RRMEnable"] = mtkwifi.token_set(cfgs["RRMEnable"],vif_idx,"") + cfgs["DLSCapable"] = mtkwifi.token_set(cfgs["DLSCapable"],vif_idx,"") + cfgs["APSDCapable"] = mtkwifi.token_set(cfgs["APSDCapable"],vif_idx,"") + cfgs["FragThreshold"] = mtkwifi.token_set(cfgs["FragThreshold"],vif_idx,"") + cfgs["RTSThreshold"] = mtkwifi.token_set(cfgs["RTSThreshold"],vif_idx,"") + cfgs["VHT_SGI"] = mtkwifi.token_set(cfgs["VHT_SGI"],vif_idx,"") + cfgs["VHT_BW_SIGNAL"] = mtkwifi.token_set(cfgs["VHT_BW_SIGNAL"],vif_idx,"") + cfgs["HT_PROTECT"] = mtkwifi.token_set(cfgs["HT_PROTECT"],vif_idx,"") + cfgs["HT_GI"] = mtkwifi.token_set(cfgs["HT_GI"],vif_idx,"") + cfgs["HT_OpMode"] = mtkwifi.token_set(cfgs["HT_OpMode"],vif_idx,"") + --cfgs["HT_TxStream"] = mtkwifi.token_set(cfgs["HT_TxStream"],vif_idx,"") + --cfgs["HT_RxStream"] = mtkwifi.token_set(cfgs["HT_RxStream"],vif_idx,"") + cfgs["HT_AMSDU"] = mtkwifi.token_set(cfgs["HT_AMSDU"],vif_idx,"") + cfgs["HT_AutoBA"] = mtkwifi.token_set(cfgs["HT_AutoBA"],vif_idx,"") + cfgs["IgmpSnEnable"] = mtkwifi.token_set(cfgs["IgmpSnEnable"],vif_idx,"") + cfgs["WirelessMode"] = mtkwifi.token_set(cfgs["WirelessMode"],vif_idx,"") + cfgs["WdsEnable"] = mtkwifi.token_set(cfgs["WdsEnable"],vif_idx,"") + cfgs["MuOfdmaDlEnable"] = mtkwifi.token_set(cfgs["MuOfdmaDlEnable"],vif_idx,"") + cfgs["MuOfdmaUlEnable"] = mtkwifi.token_set(cfgs["MuOfdmaUlEnable"],vif_idx,"") + cfgs["MuMimoDlEnable"] = mtkwifi.token_set(cfgs["MuMimoDlEnable"],vif_idx,"") + cfgs["MuMimoUlEnable"] = mtkwifi.token_set(cfgs["MuMimoUlEnable"],vif_idx,"") + +end + +function vif_del(dev, vif) + debug_write("vif_del("..dev..vif..")") + local devname,vifname = dev, vif + debug_write("devname="..devname) + debug_write("vifname="..vifname) + local devs = mtkwifi.get_all_devs() + local idx = devs[devname]["vifs"][vifname].vifidx -- or tonumber(string.match(vifname, "%d+")) + 1 + debug_write("idx="..idx, devname, vifname) + local profile = devs[devname].profile + assert(profile) + if idx and tonumber(idx) >= 0 then + local cfgs = mtkwifi.load_profile(profile) + __delete_mbss_para(cfgs, idx) + if cfgs then + debug_write("ssid"..idx.."="..cfgs["SSID"..idx].."
") + cfgs["SSID"..idx] = "" + debug_write("ssid"..idx.."="..cfgs["SSID"..idx].."
") + debug_write("wpapsk"..idx.."="..cfgs["WPAPSK"..idx].."
") + cfgs["WPAPSK"..idx] = "" + local ssidlist = {} + local j = 1 + for i = 1,16 do + if cfgs["SSID"..i] ~= "" then + ssidlist[j] = cfgs["SSID"..i] + j = j + 1 + end + end + for i,v in ipairs(ssidlist) do + debug_write("ssidlist"..i.."="..v) + end + debug_write("cfgs.BssidNum="..cfgs.BssidNum.." #ssidlist="..#ssidlist) + assert(tonumber(cfgs.BssidNum) == #ssidlist + 1, "BssidNum="..cfgs.BssidNum.." SSIDlist="..#ssidlist..", BssidNum count does not match with SSIDlist count.") + cfgs.BssidNum = #ssidlist + for i = 1,16 do + if i <= cfgs.BssidNum then + cfgs["SSID"..i] = ssidlist[i] + elseif cfgs["SSID"..i] then + cfgs["SSID"..i] = "" + end + end + + __mtkwifi_save_profile(cfgs, profile, false) + else + debug_write(profile.." cannot be found!") + end + end + luci.http.redirect(luci.dispatcher.build_url("admin", "network", "wifi")) +end + +function vif_disable(iface) + os.execute("ifconfig "..iface.." down") + luci.http.redirect(luci.dispatcher.build_url("admin", "network", "wifi")) +end + +function vif_enable(iface) + os.execute("ifconfig "..iface.." up") + luci.http.redirect(luci.dispatcher.build_url("admin", "network", "wifi")) +end + + +--[[ +-- security config in mtk wifi is quite complicated! +-- cfgs listed below are attached with vif and combined like "0;0;0;0". They need specicial treatment. + TxRate, WmmCapable, NoForwarding, + HideSSID, IEEE8021X, PreAuth, + AuthMode, EncrypType, RekeyMethod, + RekeyInterval, PMKCachePeriod, + DefaultKeyId, Key{n}Type, HT_EXTCHA, + RADIUS_Server, RADIUS_Port, +]] + +local function conf_wep_keys(cfgs,vifidx) + cfgs.DefaultKeyID = mtkwifi.token_set(cfgs.DefaultKeyID, vifidx, http.formvalue("__DefaultKeyID") or 1) + cfgs["Key1Str"..vifidx] = http.formvalue("Key1Str"..vifidx) + cfgs["Key2Str"..vifidx] = http.formvalue("Key2Str"..vifidx) + cfgs["Key3Str"..vifidx] = http.formvalue("Key3Str"..vifidx) + cfgs["Key4Str"..vifidx] = http.formvalue("Key4Str"..vifidx) + + cfgs["Key1Type"]=mtkwifi.token_set(cfgs["Key1Type"],vifidx, http.formvalue("WEP1Type"..vifidx)) + cfgs["Key2Type"]=mtkwifi.token_set(cfgs["Key2Type"],vifidx, http.formvalue("WEP2Type"..vifidx)) + cfgs["Key3Type"]=mtkwifi.token_set(cfgs["Key3Type"],vifidx, http.formvalue("WEP3Type"..vifidx)) + cfgs["Key4Type"]=mtkwifi.token_set(cfgs["Key4Type"],vifidx, http.formvalue("WEP4Type"..vifidx)) + + return cfgs +end + +local function __security_cfg(cfgs, vif_idx) + -- Reset/Clear all necessary settings here. Later, these settings will be set as per AuthMode. + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "DISABLE") + cfgs.IEEE8021X = mtkwifi.token_set(cfgs.IEEE8021X, vif_idx, "0") + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, "0") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, "0") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, "0") + + -- Update the settings which are not dependent on AuthMode + cfgs.HideSSID = mtkwifi.token_set(cfgs.HideSSID, vif_idx, http.formvalue("__hidessid") or "0") + cfgs.NoForwarding = mtkwifi.token_set(cfgs.NoForwarding, vif_idx, http.formvalue("__noforwarding") or "0") + cfgs.WmmCapable = mtkwifi.token_set(cfgs.WmmCapable, vif_idx, http.formvalue("__wmmcapable") or "0") + cfgs.TxRate = mtkwifi.token_set(cfgs.TxRate, vif_idx, http.formvalue("__txrate") or "0"); + cfgs.RekeyInterval = mtkwifi.token_set(cfgs.RekeyInterval, vif_idx, http.formvalue("__rekeyinterval") or "0"); + + local __authmode = http.formvalue("__authmode") or "Disable" + cfgs.AuthMode = mtkwifi.token_set(cfgs.AuthMode, vif_idx, __authmode) + cfgs.PMFMFPC = "0" + cfgs.PMFMFPR = "0" + cfgs.PMFSHA256 = "0" + + if __authmode == "Disable" then + cfgs.AuthMode = mtkwifi.token_set(cfgs.AuthMode, vif_idx, "OPEN") + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "NONE") + + elseif __authmode == "OPEN" or __authmode == "SHARED" or __authmode == "WEPAUTO" then + cfgs.WscModeOption = "0" + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "WEP") + cfgs = conf_wep_keys(cfgs,vif_idx) + + elseif __authmode == "Enhanced Open" then + cfgs.AuthMode = mtkwifi.token_set(cfgs.AuthMode, vif_idx, "OWE") + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "AES") + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, "1") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, "1") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, "0") + + elseif __authmode == "WPAPSK" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, http.formvalue("__encrypttype") or "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + + elseif __authmode == "WPAPSKWPA2PSK" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, http.formvalue("__encrypttype") or "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + + elseif __authmode == "WPA2PSK" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, http.formvalue("__encrypttype") or "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + -- for DOT11W_PMF_SUPPORT + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, http.formvalue("__pmfmfpc") or "0") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, http.formvalue("__pmfmfpr") or "0") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, http.formvalue("__pmfsha256") or "0") + + elseif __authmode == "WPA3PSK" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + -- for DOT11W_PMF_SUPPORT + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, "1") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, "1") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, "0") + + elseif __authmode == "WPA2PSKWPA3PSK" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + -- for DOT11W_PMF_SUPPORT + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, "1") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, "0") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, "0") + + elseif __authmode == "WPA2" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, http.formvalue("__encrypttype") or "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + cfgs.RADIUS_Server = mtkwifi.token_set(cfgs.RADIUS_Server, vif_idx, http.formvalue("__radius_server") or "0") + cfgs.RADIUS_Port = mtkwifi.token_set(cfgs.RADIUS_Port, vif_idx, http.formvalue("__radius_port") or "0") + cfgs.session_timeout_interval = mtkwifi.token_set(cfgs.session_timeout_interval, vif_idx, http.formvalue("__session_timeout_interval") or "0") + cfgs.PMKCachePeriod = mtkwifi.token_set(cfgs.PMKCachePeriod, vif_idx, http.formvalue("__pmkcacheperiod") or "0") + cfgs.PreAuth = mtkwifi.token_set(cfgs.PreAuth, vif_idx, http.formvalue("__preauth") or "0") + -- for DOT11W_PMF_SUPPORT + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, http.formvalue("__pmfmfpc") or "0") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, http.formvalue("__pmfmfpr") or "0") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, http.formvalue("__pmfsha256") or "0") + + elseif __authmode == "WPA3" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + cfgs.RADIUS_Server = mtkwifi.token_set(cfgs.RADIUS_Server, vif_idx, http.formvalue("__radius_server") or "0") + cfgs.RADIUS_Port = mtkwifi.token_set(cfgs.RADIUS_Port, vif_idx, http.formvalue("__radius_port") or "0") + cfgs.session_timeout_interval = mtkwifi.token_set(cfgs.session_timeout_interval, vif_idx, http.formvalue("__session_timeout_interval") or "0") + cfgs.PMKCachePeriod = mtkwifi.token_set(cfgs.PMKCachePeriod, vif_idx, http.formvalue("__pmkcacheperiod") or "0") + cfgs.PreAuth = mtkwifi.token_set(cfgs.PreAuth, vif_idx, http.formvalue("__preauth") or "0") + -- for DOT11W_PMF_SUPPORT + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, "1") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, "1") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, "0") + + elseif __authmode == "WPA3-192-bit" then + cfgs.AuthMode = mtkwifi.token_set(cfgs.AuthMode, vif_idx, "WPA3-192") + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "GCMP256") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + cfgs.RADIUS_Server = mtkwifi.token_set(cfgs.RADIUS_Server, vif_idx, http.formvalue("__radius_server") or "0") + cfgs.RADIUS_Port = mtkwifi.token_set(cfgs.RADIUS_Port, vif_idx, http.formvalue("__radius_port") or "0") + cfgs.session_timeout_interval = mtkwifi.token_set(cfgs.session_timeout_interval, vif_idx, http.formvalue("__session_timeout_interval") or "0") + cfgs.PMKCachePeriod = mtkwifi.token_set(cfgs.PMKCachePeriod, vif_idx, http.formvalue("__pmkcacheperiod") or "0") + cfgs.PreAuth = mtkwifi.token_set(cfgs.PreAuth, vif_idx, http.formvalue("__preauth") or "0") + -- for DOT11W_PMF_SUPPORT + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, "1") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, "1") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, "0") + + elseif __authmode == "WPA1WPA2" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, http.formvalue("__encrypttype") or "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + cfgs.RADIUS_Server = mtkwifi.token_set(cfgs.RADIUS_Server, vif_idx, http.formvalue("__radius_server") or "0") + cfgs.RADIUS_Port = mtkwifi.token_set(cfgs.RADIUS_Port, vif_idx, http.formvalue("__radius_port") or "1812") + cfgs.session_timeout_interval = mtkwifi.token_set(cfgs.session_timeout_interval, vif_idx, http.formvalue("__session_timeout_interval") or "0") + cfgs.PMKCachePeriod = mtkwifi.token_set(cfgs.PMKCachePeriod, vif_idx, http.formvalue("__pmkcacheperiod") or "0") + cfgs.PreAuth = mtkwifi.token_set(cfgs.PreAuth, vif_idx, http.formvalue("__preauth") or "0") + + elseif __authmode == "IEEE8021X" then + cfgs.AuthMode = mtkwifi.token_set(cfgs.AuthMode, vif_idx, "OPEN") + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, http.formvalue("__8021x_wep") and "WEP" or "NONE") + cfgs.IEEE8021X = mtkwifi.token_set(cfgs.IEEE8021X, vif_idx, "1") + cfgs.RADIUS_Server = mtkwifi.token_set(cfgs.RADIUS_Server, vif_idx, http.formvalue("__radius_server") or "0") + cfgs.RADIUS_Port = mtkwifi.token_set(cfgs.RADIUS_Port, vif_idx, http.formvalue("__radius_port") or "0") + cfgs.session_timeout_interval = mtkwifi.token_set(cfgs.session_timeout_interval, vif_idx, http.formvalue("__session_timeout_interval") or "0") + + elseif __authmode == "WAICERT" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "SMS4") + cfgs.Wapiifname = mtkwifi.token_set(cfgs.Wapiifname, vif_idx, "br-lan") + -- cfgs.wapicert_asipaddr + -- cfgs.WapiAsPort + -- cfgs.wapicert_ascert + -- cfgs.wapicert_usercert + + elseif __authmode == "WAIPSK" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "SMS4") + -- cfgs.wapipsk_keytype + -- cfgs.wapipsk_prekey + end +end + +function initialize_multiBssParameters(cfgs,vif_idx) + cfgs["WPAPSK"..vif_idx]="12345678" + cfgs["Key1Type"]=mtkwifi.token_set(cfgs["Key1Type"],vif_idx,"0") + cfgs["Key2Type"]=mtkwifi.token_set(cfgs["Key2Type"],vif_idx,"0") + cfgs["Key3Type"]=mtkwifi.token_set(cfgs["Key3Type"],vif_idx,"0") + cfgs["Key4Type"]=mtkwifi.token_set(cfgs["Key4Type"],vif_idx,"0") + cfgs["RADIUS_Server"]=mtkwifi.token_set(cfgs["RADIUS_Server"],vif_idx,"0") + cfgs["RADIUS_Port"]=mtkwifi.token_set(cfgs["RADIUS_Port"],vif_idx,"1812") + cfgs["RADIUS_Key"..vif_idx]="ralink" + cfgs["DefaultKeyID"]=mtkwifi.token_set(cfgs["DefaultKeyID"],vif_idx,"1") + cfgs["IEEE8021X"]=mtkwifi.token_set(cfgs["IEEE8021X"],vif_idx,"0") + cfgs["WscConfMode"]=mtkwifi.token_set(cfgs["WscConfMode"],vif_idx,"0") + cfgs["PreAuth"]=mtkwifi.token_set(cfgs["PreAuth"],vif_idx,"0") + return cfgs +end + +function __wps_ap_pbc_start_all(ifname) + os.execute("iwpriv "..ifname.." set WscMode=2"); + os.execute("iwpriv "..ifname.." set WscGetConf=1"); +end + +function __wps_ap_pin_start_all(ifname, pincode) + os.execute("iwpriv "..ifname.." set WscMode=1") + os.execute("iwpriv "..ifname.." set WscPinCode="..pincode) + os.execute("iwpriv "..ifname.." set WscGetConf=1") +end + +function __apply_wifi_wpsconf(devs, devname, cfgs, diff) + local saved = cfgs.WscConfMode and cfgs.WscConfMode:gsub(";-(%d);-","%1") or "" + local applied = diff.WscConfMode and diff["WscConfMode"][2]:gsub(";-(%d);-","%1") or "" + local num_ifs = tonumber(cfgs.BssidNum) or 0 + + for idx=1, num_ifs do + local ifname = devs[devname]["vifs"][idx]["vifname"] + if mtkwifi.__any_wsc_enabled(saved:sub(idx,idx)) == 1 then + cfgs.WscConfStatus = mtkwifi.token_set(cfgs.WscConfStatus, idx, "2") + else + cfgs.WscConfStatus = mtkwifi.token_set(cfgs.WscConfStatus, idx, "1") + end + if (diff.WscConfMode) and saved:sub(idx,idx) ~= applied:sub(idx,idx) then + cfgs = mtkwifi.__restart_if_wps(devname, ifname, cfgs) + end + end + + -- __mtkwifi_save_profile() is called outside the loop because it is a high time consuming function. + __mtkwifi_save_profile(cfgs, devs[devname]["profile"], false) + +end + +function __set_wifi_wpsconf(cfgs, wsc_enable, vif_idx) + debug_write("__set_wifi_wpsconf : wsc_enable = ",wsc_enable) + if(wsc_enable == "1") then + cfgs["WscConfMode"] = mtkwifi.token_set(cfgs["WscConfMode"], vif_idx, "7") + else + cfgs["WscConfMode"] = mtkwifi.token_set(cfgs["WscConfMode"], vif_idx, "0") + end + if(((http.formvalue("__authmode")=="OPEN") and + (http.formvalue("__encrypttype") == "WEP")) or + (http.formvalue("__hidessid") == "1")) then + cfgs.WscConfMode = mtkwifi.token_set(cfgs.WscConfMode, vif_idx, "0") + end + debug_write("__set_wifi_wpsconf : WscConfMode = ",cfgs["WscConfMode"]) +end + +function __update_mbss_para(cfgs, vif_idx) + debug_write("update_mbss_para vif: ", vif_idx) + cfgs.HT_STBC = mtkwifi.token_set(cfgs.HT_STBC, vif_idx, http.formvalue("__ht_stbc") or "0") + cfgs.HT_LDPC = mtkwifi.token_set(cfgs.HT_LDPC, vif_idx, http.formvalue("__ht_ldpc") or "0") + cfgs.VHT_STBC = mtkwifi.token_set(cfgs.VHT_STBC, vif_idx, http.formvalue("__vht_stbc") or "0") + cfgs.VHT_LDPC = mtkwifi.token_set(cfgs.VHT_LDPC, vif_idx, http.formvalue("__vht_ldpc") or "0") + cfgs.DLSCapable = mtkwifi.token_set(cfgs.DLSCapable, vif_idx, http.formvalue("__dls_capable") or "0") + cfgs.RRMEnable = mtkwifi.token_set(cfgs.RRMEnable, vif_idx, http.formvalue("__rrmenable") or "0") + cfgs.APSDCapable = mtkwifi.token_set(cfgs.APSDCapable, vif_idx, http.formvalue("__apsd_capable") or "0") + cfgs.FragThreshold = mtkwifi.token_set(cfgs.FragThreshold, vif_idx, http.formvalue("__frag_threshold") or "0") + cfgs.KickStaRssiLow = mtkwifi.token_set(cfgs.KickStaRssiLow, vif_idx, http.formvalue("__kickrssi") or "0") + cfgs.AssocReqRssiThres = mtkwifi.token_set(cfgs.AssocReqRssiThres, vif_idx, http.formvalue("__assocthres") or "0") + cfgs.RTSThreshold = mtkwifi.token_set(cfgs.RTSThreshold, vif_idx, http.formvalue("__rts_threshold") or "0") + cfgs.VHT_SGI = mtkwifi.token_set(cfgs.VHT_SGI, vif_idx, http.formvalue("__vht_sgi") or "0") + cfgs.VHT_BW_SIGNAL = mtkwifi.token_set(cfgs.VHT_BW_SIGNAL, vif_idx, http.formvalue("__vht_bw_signal") or "0") + cfgs.HT_PROTECT = mtkwifi.token_set(cfgs.HT_PROTECT, vif_idx, http.formvalue("__ht_protect") or "0") + cfgs.HT_GI = mtkwifi.token_set(cfgs.HT_GI, vif_idx, http.formvalue("__ht_gi") or "0") + cfgs.HT_OpMode = mtkwifi.token_set(cfgs.HT_OpMode, vif_idx, http.formvalue("__ht_opmode") or "0") + cfgs.HT_AMSDU = mtkwifi.token_set(cfgs.HT_AMSDU, vif_idx, http.formvalue("__ht_amsdu") or "0") + cfgs.HT_AutoBA = mtkwifi.token_set(cfgs.HT_AutoBA, vif_idx, http.formvalue("__ht_autoba") or "0") + cfgs.IgmpSnEnable = mtkwifi.token_set(cfgs.IgmpSnEnable, vif_idx, http.formvalue("__igmp_snenable") or "0") + cfgs.WirelessMode = mtkwifi.token_set(cfgs.WirelessMode, vif_idx, http.formvalue("__wirelessmode") or "0") + cfgs.WdsEnable = mtkwifi.token_set(cfgs.WdsEnable, vif_idx, http.formvalue("__wdsenable") or "0") + cfgs.MuOfdmaDlEnable = mtkwifi.token_set(cfgs.MuOfdmaDlEnable, vif_idx, http.formvalue("__muofdma_dlenable") or "0") + cfgs.MuOfdmaUlEnable = mtkwifi.token_set(cfgs.MuOfdmaUlEnable, vif_idx, http.formvalue("__muofdma_ulenable") or "0") + cfgs.MuMimoDlEnable = mtkwifi.token_set(cfgs.MuMimoDlEnable, vif_idx, http.formvalue("__mumimo_dlenable") or "0") + cfgs.MuMimoUlEnable = mtkwifi.token_set(cfgs.MuMimoUlEnable, vif_idx, http.formvalue("__mumimo_ulenable") or "0") + +end + +function vif_cfg(dev, vif) + local devname, vifname = dev, vif + if not devname then devname = vif end + debug_write("devname="..devname) + debug_write("vifname="..(vifname or "")) + local devs = mtkwifi.get_all_devs() + local profile = devs[devname].profile + assert(profile) + + --local ssid_index; + --ssid_index = devs[devname]["vifs"][vifname].vifidx + + local cfgs = mtkwifi.load_profile(profile) + + --for k,v in pairs(http.formvalue()) do + -- if type(v) == type("") or type(v) == type(0) then + -- nixio.syslog("debug", "post."..k.."="..tostring(v)) + -- else + -- nixio.syslog("debug", "post."..k.." invalid, type="..type(v)) + -- end + --end + + -- sometimes vif_idx start from 0, like AccessPolicy0 + -- sometimes it starts from 1, like WPAPSK1. nice! + local vif_idx + local to_url + if http.formvalue("__action") == "vif_cfg_view" then + vif_idx = devs[devname]["vifs"][vifname].vifidx + debug_write("vif_idx=", vif_idx, devname, vifname) + to_url = luci.dispatcher.build_url("admin", "network", "wifi", "vif_cfg_view", devname, vifname) + elseif http.formvalue("__action") == "vif_add_view" then + cfgs.BssidNum = tonumber(cfgs.BssidNum) + 1 + vif_idx = tonumber(cfgs.BssidNum) + to_url = luci.dispatcher.build_url("admin", "network", "wifi") + -- initializing ; separated parameters for the new interface + cfgs = initialize_multiBssParameters(cfgs, vif_idx) + end + assert(vif_idx) + assert(to_url) + -- "__" should not be the prefix of a name if user wants to copy form value data directly to the dat file variable + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + nixio.syslog("err", "vif_cfg, invalid value type for "..k..","..type(v)) + elseif string.byte(k) ~= string.byte("_") then + --debug_write("vif_cfg: Copying",k,v) + cfgs[k] = v or "" + end + end + + -- WDS + -- Update WdsXKey if respective WdsEncrypType is NONE + for i=0,3 do + if (cfgs["Wds"..i.."Key"] and cfgs["Wds"..i.."Key"] ~= "") and + ((not mtkwifi.token_get(cfgs["WdsEncrypType"],i+1,nil)) or + ("NONE" == mtkwifi.token_get(cfgs["WdsEncrypType"],i+1,nil))) then + cfgs["Wds"..i.."Key"] = "" + end + end + + cfgs["AccessPolicy"..vif_idx-1] = http.formvalue("__accesspolicy") + local t = mtkwifi.parse_mac(http.formvalue("__maclist")) + cfgs["AccessControlList"..vif_idx-1] = table.concat(t, ";") + + __security_cfg(cfgs, vif_idx) + __update_mbss_para(cfgs, vif_idx) + __set_wifi_wpsconf(cfgs, http.formvalue("WPSRadio"), vif_idx) + + __mtkwifi_save_profile(cfgs, profile, false) + if http.formvalue("__apply") then + mtkwifi.__run_in_child_env(__mtkwifi_reload, devname) + luci.http.redirect(luci.dispatcher.build_url("admin", "network", "wifi", "loading",to_url)) + else + luci.http.redirect(to_url) + end +end + +function string.tohex(str) + return (str:gsub('.', function (c) + return string.format('%02X', string.byte(c)) + end)) +end + +function unencode_ssid(raw_ssid) + local c + local output = "" + local convertNext = 0 + for c in raw_ssid:gmatch"." do + if(convertNext == 0) then + if(c == '+') then + output = output..' ' + elseif(c == '%') then + convertNext = 1 + else + output = output..c + end + else + output = output..string.tohex(c) + convertNext = 0 + end + end + return output +end + +function decode_ssid(raw_ssid) + local output = raw_ssid + output = output:gsub("&", "&") + output = output:gsub("<", "<") + output = output:gsub(">", ">") + output = output:gsub(""", "\"") + output = output:gsub("'", "'") + output = output:gsub(" ", " ") + for codenum in raw_ssid:gmatch("&#(%d+);") do + output = output:gsub("&#"..codenum..";", string.char(tonumber(codenum))) + end + return output +end + +function get_apcli_conn_info(ifname) + local rsp = {} + if not ifname then + rsp["conn_state"]="Disconnected" + else + local flags = tonumber(mtkwifi.read_pipe("cat /sys/class/net/"..ifname.."/flags 2>/dev/null")) or 0 + rsp["infc_state"] = flags%2 == 1 and "up" or "down" + local iwapcli = mtkwifi.read_pipe("iwconfig "..ifname.." | grep ESSID 2>/dev/null") + local ssid = string.match(iwapcli, "ESSID:\"(.*)\"") + iwapcli = mtkwifi.read_pipe("iwconfig "..ifname.." | grep 'Access Point' 2>/dev/null") + local bssid = string.match(iwapcli, "%x%x:%x%x:%x%x:%x%x:%x%x:%x%x") + if not ssid or ssid == "" then + rsp["conn_state"]= "Disconnected" + else + rsp["conn_state"] = "Connected" + rsp["ssid"] = ssid + rsp["bssid"] = bssid or "N/A" + end + end + http.write_json(rsp) +end + +function sta_info(ifname) + local output = {} + local stalist = c_StaInfo(ifname) + + local ifname_t = {["ifname"] = ifname} + table.insert(output, ifname_t) + + local count = 0 + for _ in pairs(stalist) do count = count + 1 end + + local hosts = sys.net.host_hints() + + for i=0, count - 1 do + if stalist[i].MacAddr then + if hosts[stalist[i].MacAddr] then + stalist[i].ipv4 = hosts[stalist[i].MacAddr].ipv4 or "" + stalist[i].ipv6 = hosts[stalist[i].MacAddr].ipv6 or "" + stalist[i].hostname = hosts[stalist[i].MacAddr].name or "-" + else + stalist[i].ipv4 = "" + stalist[i].ipv6 = "" + stalist[i].hostname = "-" + end + table.insert(output, stalist[i]) + end + stalist[i].security = stalist[i].AuthMode.."-"..stalist[i].EncryptMode + end + http.write_json(output) +end + +function apcli_scan(ifname) + local aplist = mtkwifi.scan_ap(ifname) + local convert=""; + for i=1, #aplist do + convert = c_convert_string_display(aplist[i]["ssid"]) + aplist[i]["original_ssid"] = aplist[i]["ssid"] + aplist[i]["ssid"] = convert["output"] + end + http.write_json(aplist) +end + +function get_station_list() + http.write("get_station_list") +end + +function reset_wifi(devname) + if devname then + os.execute("cp -f /rom/etc/wireless/"..devname.."/ /etc/wireless/") + else + os.execute("cp -rf /rom/etc/wireless /etc/") + end + return luci.http.redirect(luci.dispatcher.build_url("admin", "network", "wifi")) +end + +function reload_wifi(devname) + profiles = mtkwifi.search_dev_and_profile() + path = profiles[devname] + if mtkwifi.exists("/tmp/mtk/wifi/"..string.match(path, "([^/]+)\.dat")..".last") then + os.execute("rm -rf /tmp/mtk/wifi/"..string.match(path, "([^/]+)\.dat")..".last") + end + mtkwifi.__run_in_child_env(__mtkwifi_reload, devname) + local url_to_visit_after_reload = luci.dispatcher.build_url("admin", "network", "wifi") + luci.http.redirect(luci.dispatcher.build_url("admin", "network", "wifi", "loading",url_to_visit_after_reload)) +end + +function get_raw_profile() + local sid = http.formvalue("sid") + http.write_json("get_raw_profile") +end + +function get_country_region_list() + local mode = http.formvalue("mode") + local cr_list; + + if mtkwifi.band(mode) == "5G" then + cr_list = mtkwifi.CountryRegionList_5G_All + elseif mtkwifi.band(mode) == "6G" then + cr_list = mtkwifi.CountryRegionList_6G_All + else + cr_list = mtkwifi.CountryRegionList_2G_All + end + + http.write_json(cr_list) +end + +function remove_ch_by_region(ch_list, region) + for i = #ch_list,2,-1 do + if not ch_list[i].region[region] then + table.remove(ch_list, i) + end + end +end + +function get_channel_list() + local mode = http.formvalue("mode") + local region = tonumber(http.formvalue("country_region")) or 1 + local ch_list + + if mtkwifi.band(mode) == "5G" then + ch_list = mtkwifi.ChannelList_5G_All + elseif mtkwifi.band(mode) == "6G" then + ch_list = mtkwifi.ChannelList_6G_All + else + ch_list = mtkwifi.ChannelList_2G_All + end + + remove_ch_by_region(ch_list, region) + + for k,v in ipairs(ch_list) do + v.text = i18n.translate(v.text) + end + + http.write_json(ch_list) +end + +function get_HT_ext_channel_list() + local mode = http.formvalue("mode") + local ch_cur = tonumber(http.formvalue("ch_cur")) + local region = tonumber(http.formvalue("country_region")) or 1 + local ext_ch_list = {} + + if mtkwifi.band(mode) == "6G" then -- 6G Channel + local ch_list = mtkwifi.ChannelList_6G_All + local ext_ch_idx = -1 + local len = 0 + + for k, v in ipairs(ch_list) do + len = len + 1 + if v.channel == ch_cur then + ext_ch_idx = (k % 2 == 0) and k + 1 or k - 1 + end + end + + if ext_ch_idx > 0 and ext_ch_idx < len and ch_list[ext_ch_idx].region[region] then + ext_ch_list[1] = {} + ext_ch_list[1].val = ext_ch_idx % 2 + ext_ch_list[1].text = ch_list[ext_ch_idx].text + end + + elseif mtkwifi.band(mode) == "2.4G" then -- 2.4G Channel + local ch_list = mtkwifi.ChannelList_2G_All + local below_ch = ch_cur - 4 + local above_ch = ch_cur + 4 + local i = 1 + + if below_ch > 0 and ch_list[below_ch + 1].region[region] then + ext_ch_list[i] = {} + ext_ch_list[i].val = 0 + ext_ch_list[i].text = ch_list[below_ch + 1].text + i = i + 1 + end + + if above_ch <= 14 and ch_list[above_ch + 1].region[region] then + ext_ch_list[i] = {} + ext_ch_list[i].val = 1 + ext_ch_list[i].text = ch_list[above_ch + 1].text + end + else -- 5G Channel + local ch_list = mtkwifi.ChannelList_5G_All + local ext_ch_idx = -1 + local len = 0 + + for k, v in ipairs(ch_list) do + len = len + 1 + if v.channel == ch_cur then + ext_ch_idx = (k % 2 == 0) and k + 1 or k - 1 + end + end + + if ext_ch_idx > 0 and ext_ch_idx < len and ch_list[ext_ch_idx].region[region] then + ext_ch_list[1] = {} + ext_ch_list[1].val = ext_ch_idx % 2 + ext_ch_list[1].text = ch_list[ext_ch_idx].text + end + end + + for k,v in ipairs(ext_ch_list) do + v.text = i18n.translate(v.text) + end + + http.write_json(ext_ch_list) +end + +function get_5G_2nd_80Mhz_channel_list() + local ch_cur = tonumber(http.formvalue("ch_cur")) + local region = tonumber(http.formvalue("country_region")) + local ch_list = mtkwifi.ChannelList_5G_2nd_80MHZ_ALL + local ch_list_5g = mtkwifi.ChannelList_5G_All + local i, j, test_ch, test_idx + local bw80_1st_idx = -1 + + -- remove adjacent freqencies starting from list tail. + for i = #ch_list,1,-1 do + for j = 0,3 do + if ch_list[i].channel == -1 then + break + end + + test_ch = ch_list[i].channel + j * 4 + test_idx = ch_list[i].chidx + j + + if test_ch == ch_cur then + if i + 1 <= #ch_list and ch_list[i + 1] then + table.remove(ch_list, i + 1) + end + table.remove(ch_list, i) + bw80_1st_idx = i + break + end + + if i == (bw80_1st_idx - 1) or (not ch_list_5g[test_idx].region[region]) then + table.remove(ch_list, i) + break + end + end + end + + -- remove unused channel. + for i = #ch_list,1,-1 do + if ch_list[i].channel == -1 then + table.remove(ch_list, i) + end + end + + for k,v in ipairs(ch_list) do + v.text = i18n.translate(v.text) + end + + http.write_json(ch_list) +end + +function apcli_cfg(dev, vif) + local devname = dev + debug_write(devname) + local profiles = mtkwifi.search_dev_and_profile() + debug_write(profiles[devname]) + assert(profiles[devname]) + + local cfgs = mtkwifi.load_profile(profiles[devname]) + + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + nixio.syslog("err", "apcli_cfg, invalid value type for "..k..","..type(v)) + elseif string.byte(k) ~= string.byte("_") then + cfgs[k] = v or "" + end + end + + -- http.write_json(http.formvalue()) + + -- Mediatek Adaptive Network + --[=[ moved to a separated page + if cfgs.ApCliEzEnable then + cfgs.EzEnable = cfgs.ApCliEzEnable + cfgs.ApMWDS = cfgs.ApCliMWDS + cfgs.EzConfStatus = cfgs.ApCliEzConfStatus + cfgs.EzOpenGroupID = cfgs.ApCliEzOpenGroupID + if http.formvalue("__group_id_mode") == "0" then + cfgs.EzGroupID = cfgs.ApCliEzGroupID + cfgs.EzGenGroupID = "" + cfgs.ApCliEzGenGroupID = "" + else + cfgs.EzGroupID = "" + cfgs.ApCliEzGroupID = "" + cfgs.EzGenGroupID = cfgs.ApCliEzGenGroupID + end + -- if dbdc + -- os.execute("app_ez &") + -- os.execute("ManDaemon ") + end + ]=] + __mtkwifi_save_profile(cfgs, profiles[devname], false) + + -- M.A.N Push parameters + -- They are not part of wifi profile, we save it into /etc/man.conf. + + --[=[ moved to a separated page + local man_ssid = http.formvalue("__man_ssid_"..vifname) + local man_pass = http.formvalue("__man_pass_"..vifname) + local man_auth = http.formvalue("__man_auth_"..vifname) or "" + + if man_ssid and man_pass then + local fp = io.open("/etc/man."..vifname..".conf", "w+") + fp:write("__man_ssid_"..vifname.."="..man_ssid.."\n") + fp:write("__man_pass_"..vifname.."="..man_pass.."\n") + fp:write("__man_auth_"..vifname.."="..man_auth.."\n") + fp:close() + end + ]=] + + -- commented, do not connect by default + --[=[ + os.execute("iwpriv apcli0 set ApCliEnable=0") + os.execute("iwpriv apcli0 set Channel="..cfgs.Channel) + os.execute("iwpriv apcli0 set ApCliAuthMode="..cfgs.ApCliAuthMode) + os.execute("iwpriv apcli0 set ApCliEncrypType="..cfgs.ApCliEncrypType) + if cfgs.ApCliAuthMode == "WEP" then + os.execute("#iwpriv apcli0 set ApCliDefaultKeyID="..cfgs.ApCliDefaultKeyID) + os.execute("#iwpriv apcli0 set ApCliKey1="..cfgs.ApCliKey1Str) + elseif cfgs.ApCliAuthMode == "WPAPSK" + or cfgs.ApCliAuthMode == "WPA2PSK" + or cfgs.ApCliAuthMode == "WPAPSKWPA2PSK" then + os.execute("iwpriv apcli0 set ApCliWPAPSK="..cfgs.ApCliWPAPSK) + end + -- os.execute("iwpriv apcli0 set ApCliWirelessMode=") + os.execute("iwpriv apcli0 set ApCliSsid="..cfgs.ApCliSsid) + os.execute("iwpriv apcli0 set ApCliEnable=1") + ]=] + if http.formvalue("__apply") then + mtkwifi.__run_in_child_env(__mtkwifi_reload, devname) + local url_to_visit_after_reload = luci.dispatcher.build_url("admin", "network", "wifi", "apcli_cfg_view", dev, vif) + luci.http.redirect(luci.dispatcher.build_url("admin", "network", "wifi", "loading",url_to_visit_after_reload)) + else + luci.http.redirect(luci.dispatcher.build_url("admin", "network", "wifi", "apcli_cfg_view", dev, vif)) + end +end + +function apcli_connect(dev, vif) + -- dev_vif can be + -- 1. mt7620.apcli0 # simple case + -- 2. mt7615e.1.apclix0 # multi-card + -- 3. mt7615e.1.2G.apclix0 # multi-card & multi-profile + local devname,vifname = dev, vif + debug_write("devname=", dev, "vifname=", vif) + local profiles = mtkwifi.search_dev_and_profile() + debug_write(profiles[devname]) + assert(profiles[devname]) + local cfgs = mtkwifi.load_profile(profiles[devname]) + cfgs.ApCliEnable = "1" + __mtkwifi_save_profile(cfgs, profiles[devname], true) + os.execute("ifconfig "..vifname.." up") + --os.execute("brctl addif br-lan "..vifname) + os.execute("iwpriv "..vifname.." set MACRepeaterEn="..cfgs.MACRepeaterEn) + os.execute("iwpriv "..vifname.." set ApCliEnable=0") + os.execute("iwpriv "..vifname.." set Channel="..cfgs.Channel) + os.execute("iwpriv "..vifname.." set ApCliAuthMode="..cfgs.ApCliAuthMode) + os.execute("iwpriv "..vifname.." set ApCliEncrypType="..cfgs.ApCliEncrypType) + if cfgs.ApCliEncrypType == "WEP" then + os.execute("iwpriv "..vifname.." set ApCliDefaultKeyID="..cfgs.ApCliDefaultKeyID) + if (cfgs.ApCliDefaultKeyID == "1") then + os.execute("iwpriv "..vifname.." set ApCliKey1=\""..mtkwifi.__handleSpecialChars(cfgs.ApCliKey1Str).."\"") + elseif (cfgs.ApCliDefaultKeyID == "2") then + os.execute("iwpriv "..vifname.." set ApCliKey2=\""..mtkwifi.__handleSpecialChars(cfgs.ApCliKey2Str).."\"") + elseif (cfgs.ApCliDefaultKeyID == "3") then + os.execute("iwpriv "..vifname.." set ApCliKey3=\""..mtkwifi.__handleSpecialChars(cfgs.ApCliKey3Str).."\"") + elseif (cfgs.ApCliDefaultKeyID == "4") then + os.execute("iwpriv "..vifname.." set ApCliKey4=\""..mtkwifi.__handleSpecialChars(cfgs.ApCliKey4Str).."\"") + end + elseif cfgs.ApCliAuthMode == "WPAPSK" + or cfgs.ApCliAuthMode == "WPA2PSK" + or cfgs.ApCliAuthMode == "WPAPSKWPA2PSK" then + os.execute("iwpriv "..vifname.." set ApCliWPAPSK=\""..mtkwifi.__handleSpecialChars(cfgs.ApCliWPAPSK).."\"") + end + os.execute("iwpriv "..vifname.." set ApCliSsid=\""..mtkwifi.__handleSpecialChars(cfgs.ApCliSsid).."\"") + os.execute("iwpriv "..vifname.." set ApCliEnable=1") + os.execute("iwpriv "..vifname.." set ApCliAutoConnect=3") + + luci.http.redirect(luci.dispatcher.build_url("admin", "network", "wifi")) +end + +function apcli_disconnect(dev, vif) + -- dev_vif can be + -- 1. mt7620.apcli0 # simple case + -- 2. mt7615e.1.apclix0 # multi-card + -- 3. mt7615e.1.2G.apclix0 # multi-card & multi-profile + local devname,vifname = dev, vif + debug_write("devname=", dev, "vifname", vif) + debug_write(devname) + debug_write(vifname) + local profiles = mtkwifi.search_dev_and_profile() + debug_write(profiles[devname]) + assert(profiles[devname]) + local cfgs = mtkwifi.load_profile(profiles[devname]) + cfgs.ApCliEnable = "1" + __mtkwifi_save_profile(cfgs, profiles[devname], true) + os.execute("iwpriv "..vifname.." set ApCliEnable=0") + os.execute("ifconfig "..vifname.." down") + --os.execute("brctl delif br-lan "..vifname) + luci.http.redirect(luci.dispatcher.build_url("admin", "network", "wifi")) +end + +function apply_power_boost_settings() + local devname = http.formvalue("__devname") + local ret_status = {} + local devs = mtkwifi.get_all_devs() + local dev = {} + for _,v in ipairs(devs) do + if v.devname == devname then + dev = v + break + end + end + if next(dev) == nil then + ret_status["status"]= "Device "..(devname or "").." not found!" + elseif not dev.isPowerBoostSupported then + ret_status["status"]= "Power Boost feature is not supported by "..(devname or "").." Device!" + else + local cfgs = mtkwifi.load_profile(dev.profile) + if type(cfgs) ~= "table" or next(cfgs) == nil then + ret_status["status"]= "Profile settings file not found!" + else + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + debug_write("ERROR: [apply_power_boost_settings] String expected; Got"..type(v).."for"..k.."key") + ret_status["status"]= "Power Boost settings are of incorrect type!" + break + elseif string.byte(k) ~= string.byte("_") then + cfgs[k] = v or "" + end + end + if next(ret_status) == nil then + if type(dev.vifs) ~= "table" or next(dev.vifs) == nil or not cfgs.BssidNum or cfgs.BssidNum == "0" then + ret_status["status"]= "No Wireless Interfaces has been added yet!" + elseif cfgs.PowerUpenable ~= "1" then + ret_status["status"]= "Power Boost feature is not enabled!" + else + local up_vif_name_list = {} + for idx,vif in ipairs(dev.vifs) do + if vif.state == "up" and vif.vifname ~= nil and vif.vifname ~= "" and type(vif.vifname) == "string" then + up_vif_name_list[idx] = vif.vifname + end + end + if next(up_vif_name_list) == nil then + ret_status["status"]= "No Wireless Interfaces is up!" + else + for _,vifname in ipairs(up_vif_name_list) do + os.execute("iwpriv "..vifname.." set TxPowerBoostCtrl=0:"..cfgs.PowerUpCckOfdm) + os.execute("iwpriv "..vifname.." set TxPowerBoostCtrl=1:"..cfgs.PowerUpHT20) + os.execute("iwpriv "..vifname.." set TxPowerBoostCtrl=2:"..cfgs.PowerUpHT40) + os.execute("iwpriv "..vifname.." set TxPowerBoostCtrl=3:"..cfgs.PowerUpVHT20) + os.execute("iwpriv "..vifname.." set TxPowerBoostCtrl=4:"..cfgs.PowerUpVHT40) + os.execute("iwpriv "..vifname.." set TxPowerBoostCtrl=5:"..cfgs.PowerUpVHT80) + os.execute("iwpriv "..vifname.." set TxPowerBoostCtrl=6:"..cfgs.PowerUpVHT160) + os.execute("sleep 1") -- Wait for 1 second to let driver process the above data + end + __mtkwifi_save_profile(cfgs, dev.profile, true) + ret_status["status"]= "SUCCESS" + end + end + end + end + end + http.write_json(ret_status) +end + +function get_bssid_num(devName) + local ret_status = {} + local profiles = mtkwifi.search_dev_and_profile() + for dev,profile in pairs(profiles) do + if devName == dev then + local cfgs = mtkwifi.load_profile(profile) + if type(cfgs) ~= "table" or next(cfgs) == nil then + ret_status["status"]= "Profile settings file not found!" + else + ret_status["status"] = "SUCCESS" + ret_status["bssidNum"] = cfgs.BssidNum + end + break + end + end + if next(ret_status) == nil then + ret_status["status"]= "Device "..(devName or "").." not found!" + end + http.write_json(ret_status) +end + +local exec_reset_to_defaults_cmd = function (devname) + if devname then + os.execute("wifi reset "..devname) + else + os.execute("wifi reset") + end +end + +function reset_to_defaults(devname) + mtkwifi.__run_in_child_env(exec_reset_to_defaults_cmd, devname) + luci.http.redirect(luci.dispatcher.build_url("admin", "network", "wifi", "loading",mtkwifi.get_referer_url())) +end + diff --git a/immortalwrt-patches/package/mtk/applications/mtwifi-cfg/files/mtwifi-cfg/mtwifi_cfg b/immortalwrt-patches/package/mtk/applications/mtwifi-cfg/files/mtwifi-cfg/mtwifi_cfg new file mode 100644 index 0000000..5f62af2 --- /dev/null +++ b/immortalwrt-patches/package/mtk/applications/mtwifi-cfg/files/mtwifi-cfg/mtwifi_cfg @@ -0,0 +1,698 @@ +#!/usr/bin/lua +--[[ + * + * Copyright (C) 2023 hanwckf + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * The Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +]] + +local cjson = require "cjson" +local inspect = require "inspect" +local nixio = require "nixio" +local l1parser = require "l1dat_parser" +local datconf = require "datconf" +local utils = require "mtwifi_utils" +local defs = require "mtwifi_defs" + +local l1dat = l1parser.load_l1_profile(l1parser.L1_DAT_PATH) + +function string:split(sep) + local sep, fields = sep or ":", {} + local pattern = string.format("([^%s]+)", sep) + self:gsub(pattern, function(c) fields[#fields+1] = c end) + return fields +end + +function load_profile(path) + local cfgs = {} + + cfgobj = datconf.openfile(path) + if cfgobj then + cfgs = cfgobj:getall() + cfgobj:close() + end + + return cfgs +end + +function save_profile(cfgs, path) + if not cfgs then + return + end + + local datobj = datconf.openfile(path) + datobj:merge(cfgs) + datobj:close(true) + + os.execute("sync") +end + +function diff_cfg(cfg1, cfg2) + local diff = {} + + for k,v in pairs(cfg1) do + if tostring(cfg2[k]) ~= tostring(cfg1[k]) then + diff[k] = {cfg1[k] or "", cfg2[k] or ""} + end + end + + for k,v in pairs(cfg2) do + if tostring(cfg2[k]) ~= tostring(cfg1[k]) then + diff[k] = {cfg1[k] or "", cfg2[k] or ""} + end + end + + return diff +end + +function vif_status(ifname) + local flags = tonumber(utils.read_pipe("cat /sys/class/net/"..ifname.."/flags 2>/dev/null")) or 0 + if flags%2 == 1 then + return "up" + end + return "down" +end + +function is_dbdc_inited(ifname) + local mac = utils.trim(utils.read_pipe("cat /sys/class/net/"..ifname.."/address 2>/dev/null")) + if mac and mac ~= "00:00:00:00:00:00" then + return true + end + return false +end + +function ifup(ifname) + if ifname and #ifname > 0 then + os.execute("ifconfig "..ifname.." up") + end +end + +function ifdown(ifname) + if ifname and #ifname > 0 then + os.execute("ifconfig "..ifname.." down") + end +end + +function cfg2dat(cfg) + if type(cfg) == type(true) then + if cfg then return 1 else return 0 end + elseif type(cfg) == type(0) or type(cfg) == type("") then + return cfg + end +end + +function set_dev_dat(dats, dat, cfg) + if cfg ~= nil then + dats[dat] = cfg2dat(cfg) + end +end + +function set_dat(dats, apidx, dat, cfg) + if cfg ~= nil then + dats[dat] = utils.token_set(dats[dat], apidx, cfg2dat(cfg)) + end +end + +function set_idx_dat(dats, apidx, dat, cfg) + if cfg ~= nil then + dats[dat..tostring(apidx)] = cfg + end +end + +function vif_in_dev(vif, dev) + if (vif == dev.main_ifname + or string.match(vif, utils.esc(dev.ext_ifname).."[0-9]+") + or string.match(vif, utils.esc(dev.apcli_ifname).."[0-9]+") + or string.match(vif, utils.esc(dev.wds_ifname).."[0-9]+") + or string.match(vif, utils.esc(dev.mesh_ifname).."[0-9]+")) then + return true + else + return false + end +end + +function vif_is_apcli(vif, dev) + if (string.match(vif, utils.esc(dev.apcli_ifname).."[0-9]+")) then + return true + else + return false + end +end + +function vif_count(cfg) + local vif_num = 0 + local ap_num = 0 + local apcli_num = 0 + + for k,v in pairs(cfg.interfaces) do + vif_num = vif_num + 1 + if v.config.mode == "ap" then + ap_num = ap_num + 1 + elseif v.config.mode == "sta" then + apcli_num = apcli_num +1 + end + end + + return vif_num, ap_num, apcli_num +end + +function mtwifi_cfg_foreach_vif(cfg, mode, func) + if cfg == nil or func == nil then return end + + local vif_num,_,_ = vif_count(cfg) + for idx=0,vif_num-1 do + local v = cfg.interfaces[tostring(idx)] + if v.config.mode and v.config.mode == mode then + func(v) + end + end +end + +function __apcli_auto_connect(ifname) + __exec_iwpriv_cmd(ifname, "ApCliEnable", "1") + __exec_iwpriv_cmd(ifname, "ApCliAutoConnect", "3") +end + +function mtwifi_up(devname, cfg, restore_vifs, is_dbdc) + nixio.syslog("info", "mtwifi-cfg: start "..devname) + local dev = l1dat.devname_ridx[devname] + local dbdc_main_ifname = defs.dbdc_init_ifname + + if not dev then return end + + if is_dbdc then + if not is_dbdc_inited(dbdc_main_ifname) then + nixio.syslog("info", "mtwifi-cfg: dbdc card init...") + ifup(dbdc_main_ifname) + utils.sleep(1) + ifdown(dbdc_main_ifname) + end + + if restore_vifs and devname ~= cfg.device then + -- restart ap vif + for _,vif in ipairs(restore_vifs) do + if vif_in_dev(vif, dev) and (not vif_is_apcli(vif, dev)) then + nixio.syslog("info", "mtwifi-cfg: restore ap vif: "..vif) + ifup(vif) + end + end + -- restart apcli vif + for _,vif in ipairs(restore_vifs) do + if vif_is_apcli(vif, dev) then + nixio.syslog("info", "mtwifi-cfg: restore apcli vif: "..vif) + ifup(vif) + __apcli_auto_connect(vif) + end + end + return + end + end + + local start_vif = function(v) + if (not v.config.disabled) and v.mtwifi_ifname then + local vif = v.mtwifi_ifname + nixio.syslog("info", "mtwifi-cfg: up vif: "..vif) + ifup(vif) + end + end + + mtwifi_cfg_foreach_vif(cfg, "ap", start_vif) + mtwifi_cfg_foreach_vif(cfg, "sta", start_vif) + os.execute("startwapp.sh") +end + +function mtwifi_down(devname, cfg) + nixio.syslog("info", "mtwifi-cfg: stop "..devname) + local dev = l1dat.devname_ridx[devname] + local reset_vifs = {} + + if not dev then return end + + for _,vif in ipairs(string.split(utils.read_pipe("ls /sys/class/net"), "\n")) do + if (vif_status(vif) == "up" and vif_in_dev(vif, dev)) then + nixio.syslog("info", "mtwifi-cfg: down vif: "..vif) + ifdown(vif) + if cfg and devname ~= cfg.device then + reset_vifs[#reset_vifs+1] = vif + end + end + end + os.execute("startwapp.sh") + return reset_vifs +end + +function mtwifi_reinstall() + nixio.syslog("info", "mtwifi-cfg: unload mtwifi module...") + + --os.execute("rmmod mt_whnat") + --os.execute("rmmod mtfwd") + os.execute("rmmod mtk_warp_proxy") + os.execute("rmmod mtk_warp") + --os.execute("rmmod mt7915_mt_wifi") + os.execute("rmmod mt_wifi") + + utils.sleep(2) + + nixio.syslog("info", "mtwifi-cfg: reload mtwifi module...") + + os.execute("modprobe mt_wifi") + --os.execute("modprobe mt7915_mt_wifi") + os.execute("modprobe mtk_warp") + os.execute("modprobe mtk_warp_proxy") + --os.execute("modprobe mtfwd") + --os.execute("modprobe mt_whnat") +end + +function __exec_iwpriv_cmd(ifname, key, val) + local cmd = string.format("iwpriv %s set %s=%s", ifname, key, tostring(val)) + nixio.syslog("info", "mtwifi-cfg: iwpriv cmd: "..cmd) + os.execute(cmd) +end + +function mtwifi_cfg_iwpriv_hook(cfg) + local ap_hook = function(v) + if (not v.config.disabled) and v.mtwifi_ifname then + local vif = v.mtwifi_ifname + for k, j in pairs(defs.iwpriv_ap_cfgs) do + __exec_iwpriv_cmd(vif, j[1], v.config[k] or j[2]) + end + end + end + + local apcli_hook = function(v) + if (not v.config.disabled) and v.mtwifi_ifname then + local vif = v.mtwifi_ifname + __apcli_auto_connect(vif) + end + end + + mtwifi_cfg_foreach_vif(cfg, "ap", ap_hook) + mtwifi_cfg_foreach_vif(cfg, "sta", apcli_hook) +end + +function set_chip_cfg(cfg, dats) + local dev_dats + local uci + local dat_key + + if not cfg.config.dbdc_main then return end + + for k,v in pairs(defs.chip_cfgs) do + uci = k + dat_key = v[1] + + -- reset chip cfgs to default + dats[dat_key] = v[2] + + -- setup current dev cfg + set_dev_dat(dats, dat_key, cfg.config[uci]) + end + + -- setup other dev cfg + for k,v in pairs(l1dat.devname_ridx) do + if k ~= cfg.device then + dev_dats = load_profile(v.profile_path) + if dev_dats then + for _,j in pairs(defs.chip_cfgs) do + dat_key = j[1] + set_dev_dat(dev_dats, dat_key, dats[dat_key]) + end + save_profile(dev_dats, v.profile_path) + end + end + end +end + +function mtwifi_cfg_setup(argv) + local cfg = cjson.decode(argv) + + utils.log2file("input = " .. inspect(cfg)) + + local devname = cfg.device + local dev = l1dat.devname_ridx[devname] + + if not dev then return end + + local profile = dev.profile_path + + local dats = load_profile(profile) + if not dats then + nixio.syslog("err", "mtwifi-cfg: profile ".. profile .. "open failed") + return + end + local dats_orig = load_profile(profile) + + local vif_num, bssid_num, apcli_num = vif_count(cfg) + + if vif_num == 0 then + nixio.syslog("err", "mtwifi-cfg: not valid vif found!") + return + end + if bssid_num > defs.max_mbssid then + nixio.syslog("err", "mtwifi-cfg: too many vifs!") + return + end + if apcli_num > 1 then + nixio.syslog("err", "mtwifi-cfg: only support one apcli vif!") + end + + if bssid_num > 0 then + dats.BssidNum = bssid_num + else + dats.BssidNum = 1 + end + + -- setup apcli + dats.ApCliEnable = 0 + dats.ApCliSsid = "" + dats.ApCliBssid = "" + dats.ApCliAuthMode = "" + dats.ApCliEncrypType = "" + dats.ApCliWPAPSK = "" + dats.ApCliPMFMFPC = 0 + dats.ApCliPMFMFPR = 0 + dats.ApCliPMFSHA256 = 0 + + for _,v in pairs(cfg.interfaces) do + if v.config.mode == "sta" then + if not v.config.disabled then + dats.ApCliEnable = 1 + end + dats.ApCliSsid = v.config.ssid or "" + dats.ApCliBssid = v.config.bssid or "" + dats.ApCliAuthMode = defs.enc2dat[v.config.encryption][1] + dats.ApCliEncrypType = defs.enc2dat[v.config.encryption][2] + dats.ApCliWPAPSK = v.config.key or "" + + if dats.ApCliAuthMode == "OWE" or dats.ApCliAuthMode == "WPA3PSK" then + dats.ApCliPMFMFPC = 1 + dats.ApCliPMFMFPR = 1 + dats.ApCliPMFSHA256 = 0 + end + break + end + end + + -- setup chip cfgs + set_chip_cfg(cfg, dats) + + -- setup dev cfgs + if cfg.config.twt then + dats.TWTSupport = cfg.config.twt + else + dats.TWTSupport = 0 + end + + if type(cfg.config.country) == type("") and #cfg.config.country == 2 then + dats.CountryCode = cfg.config.country + if cfg.config.band == "2g" then + dats.CountryRegion = defs.countryRegions[cfg.config.country][1] + elseif cfg.config.band == "5g" then + dats.CountryRegionABand = defs.countryRegions[cfg.config.country][2] + end + end + + if cfg.config.channel == "auto" then + dats.AutoChannelSelect = 3 + dats.Channel = 0 + else + dats.AutoChannelSelect = 0 + dats.Channel = cfg.config.channel + end + + if cfg.config.htmode == "HT20" or cfg.config.htmode == "VHT20" or cfg.config.htmode == "HE20" then + dats.HT_BW = 0 + dats.VHT_BW = 0 + elseif cfg.config.htmode == "HT40" or cfg.config.htmode == "VHT40" or cfg.config.htmode == "HE40" then + dats.HT_BW = 1 + dats.VHT_BW = 0 + if cfg.config.noscan ~= nil and cfg.config.noscan == "1" then + dats.HT_BSSCoexistence = 0 + else + dats.HT_BSSCoexistence = 1 + end + elseif cfg.config.htmode == "VHT80" or cfg.config.htmode == "HE80" then + dats.HT_BW = 1 + dats.VHT_BW = 1 + elseif cfg.config.htmode == "VHT160" or cfg.config.htmode == "HE160" then + dats.HT_BW = 1 + dats.VHT_BW = 2 + end + -- DBDC merge quirk workaround (Cudy TR3000 / MT7981B): + -- ra0/rax0 <-> dat binding is RANDOM per boot (dat.MacAddress + -- writes flip across reboots). The driver applies PHY-correct + -- band/channel/htmode/SSID per netdev regardless of which dat it + -- bound (band-content invariant), so physical broadcast is always + -- correct: ra0 broadcasts 2.4G HE40, rax0 broadcasts 5G HE160 ch36. + -- Only UCI labels vs iwinfo can disagree. mtwifi_cfg writes + -- VHT_Sec80_Channel based on cfg.config.band (not idx), so the 5g + -- profile carries Sec80=155 whether it lands in b0.dat or b1.dat; + -- the 2g profile harmlessly carries VHT_BW=2 + Sec80=155 (Sec80 + -- ignored on 2.4G). + if cfg.config.band == "2g" then + dats.HT_BW = 1 + dats.VHT_BW = 2 + dats.VHT_Sec80_Channel = 155 + elseif cfg.config.band == "5g" and (cfg.config.htmode == "HE160" or cfg.config.htmode == "VHT160") then + dats.VHT_Sec80_Channel = 155 + end + + if cfg.config.txpower and cfg.config.txpower < 100 then + dats.PERCENTAGEenable = 1 + dats.TxPower = cfg.config.txpower + else + dats.PERCENTAGEenable = 0 + dats.TxPower = 100 + end + + if cfg.config.mu_beamformer then + dats.ETxBfEnCond = 1 + if dats.ApCliEnable == 1 then + dats.MUTxRxEnable = 3 + else + dats.MUTxRxEnable = 3 + end + dats.ITxBfEn = 1 + else + dats.ETxBfEnCond = 0 + dats.MUTxRxEnable = 0 + dats.ITxBfEn = 0 + end + + local WirelessMode + if cfg.config.band == "2g" then + if string.sub(cfg.config.htmode,1,2) == "HE" then + WirelessMode = 16 -- PHY_11AX_24G + else + WirelessMode = 9 -- PHY_11BGN_MIXED + end + elseif cfg.config.band == "5g" then + if string.sub(cfg.config.htmode,1,2) == "HE" then + WirelessMode = 17 -- PHY_11AX_5G + else + WirelessMode = 15 -- PHY_11VHT_N_MIXED + end + end + + -- reset vif cfgs to default + for k,v in pairs(defs.vif_cfgs) do + dats[k] = "" + for i = 1, bssid_num do + dats[k] = utils.token_set(dats[k], i, v) + end + end + + for k,v in pairs(defs.vif_cfgs_idx) do + for i = 1, defs.max_mbssid do + dats[k..tostring(i)] = "" + end + for i = 1, bssid_num do + dats[k..tostring(i)] = v + end + end + + for k,v in pairs(defs.vif_acl) do + for i = 0, defs.max_mbssid-1 do + dats[k..tostring(i)] = "" + end + for i = 0, bssid_num-1 do + dats[k..tostring(i)] = v + end + end + + -- setup vif cfgs + local apidx = 1 + for idx = 0,vif_num-1 do + v = cfg.interfaces[tostring(idx)] + if v.config.mode == "ap" then + set_idx_dat(dats, apidx, "SSID", v.config.ssid) + set_idx_dat(dats, apidx, "WPAPSK", v.config.key or "") + set_dat(dats, apidx, "NoForwarding", 0) + set_dat(dats, apidx, "HideSSID", v.config.hidden) + set_dat(dats, apidx, "WmmCapable", v.config.wmm) + set_dat(dats, apidx, "RRMEnable", v.config.ieee80211k) + set_dat(dats, apidx, "FtSupport", v.config.ieee80211r) + set_dat(dats, apidx, "RekeyInterval", v.config.wpa_group_rekey) + set_dat(dats, apidx, "MuMimoDlEnable",0) + set_dat(dats, apidx, "MuMimoUlEnable",0) + set_dat(dats, apidx, "MuOfdmaDlEnable",v.config.ofdma_dl) + set_dat(dats, apidx, "MuOfdmaUlEnable",v.config.ofdma_ul) + set_dat(dats, apidx, "HT_AMSDU",v.config.amsdu) + set_dat(dats, apidx, "HT_AutoBA",v.config.autoba) + set_dat(dats, apidx, "APSDCapable",v.config.uapsd) + set_dat(dats, apidx, "RTSThreshold", v.config.rts) + set_dat(dats, apidx, "FragThreshold",v.config.frag) + set_dat(dats, apidx, "WirelessMode", WirelessMode) + + if v.config.macfilter then + if v.config.macfilter == "allow" then + set_idx_dat(dats, apidx-1, "AccessPolicy", 1) + elseif v.config.macfilter == "deny" then + set_idx_dat(dats, apidx-1, "AccessPolicy", 2) + end + end + + if v.config.maclist then + local maclist = "" + for i, v in ipairs(v.config.maclist) do + if i > defs.max_acl_entry then + break + end + if #maclist > 0 then + maclist = maclist .. ";" .. v + else + maclist = v + end + end + set_idx_dat(dats, apidx-1, "AccessControlList", maclist) + end + + local authmode = defs.enc2dat[v.config.encryption][1] + set_dat(dats, apidx, "AuthMode", authmode) + set_dat(dats, apidx, "EncrypType", defs.enc2dat[v.config.encryption][2]) + + if authmode == "OWE" or authmode == "WPA3PSK" then + set_dat(dats, apidx, "PMFMFPC", 1) + set_dat(dats, apidx, "PMFMFPR", 1) + set_dat(dats, apidx, "PMFSHA256", 0) + elseif authmode == "WPA2PSKWPA3PSK" then + set_dat(dats, apidx, "PMFMFPC", 1) + set_dat(dats, apidx, "PMFMFPR", 0) + set_dat(dats, apidx, "PMFSHA256", 0) + end + + if not (authmode == "OPEN" or authmode == "OWE") then + set_dat(dats, apidx, "RekeyMethod", "TIME") + end + + apidx = apidx + 1 + end + end + + local reinstall_wifidrv = false + local cfg_diff = diff_cfg(dats_orig, dats) + utils.log2file("diff = " .. inspect(cfg_diff)) + + save_profile(dats, profile) + + for _,v in pairs(defs.reinstall_cfgs) do + if cfg_diff[v] ~= nil then + if utils.exists("/sys/module/mt_wifi") == false then + nixio.syslog("err", "mtwifi-cfg: mtwifi module is build-in, please reboot the device!") + return + end + reinstall_wifidrv = true + end + end + + if string.find(profile, "dbdc") then + if reinstall_wifidrv then + for k, _ in pairs(l1dat.devname_ridx) do + mtwifi_down(k) + end + + mtwifi_reinstall() + + for k, _ in pairs(l1dat.devname_ridx) do + if k == devname then + mtwifi_up(k, cfg, nil, true) + else + os.execute("/sbin/wifi up " .. k) -- TODO: may cause deadlock + end + end + else + local restore_vifs = {} + local restart_other_dbdc_dev = false + if next(cfg_diff) ~= nil then + restart_other_dbdc_dev = true + end + + if restart_other_dbdc_dev then + for k, _ in pairs(l1dat.devname_ridx) do + local ret = mtwifi_down(k, cfg) + for _, v in ipairs(ret) do + table.insert(restore_vifs, v) + end + end + if #restore_vifs > 0 then + nixio.syslog("info", "mtwifi-cfg: dbdc restore_vifs: "..inspect.inspect(restore_vifs)) + end + + for k, _ in pairs(l1dat.devname_ridx) do + mtwifi_up(k, cfg, restore_vifs, true) + end + else + mtwifi_down(devname) + mtwifi_up(devname, cfg, nil, true) + end + end + else + mtwifi_down(devname) + + if reinstall_wifidrv then + mtwifi_reinstall() + end + + mtwifi_up(devname, cfg) + end + + mtwifi_cfg_iwpriv_hook(cfg) +end + +function mtwifi_cfg_down(devname) + if devname then + mtwifi_down(devname) + else + for k, _ in pairs(l1dat.devname_ridx) do + mtwifi_down(k) + end + end +end + +local action = { + ["down"] = function(devname) + mtwifi_cfg_down(devname) + end, + + ["setup"] = function() + local argv = io.read() + if #argv > 0 then + mtwifi_cfg_setup(argv) + os.execute("( /etc/wifi-band-verify.sh >/dev/null 2>&1 & )") + end + end +} + +if #arg == 1 then + action[arg[1]]() +elseif #arg == 2 then + action[arg[1]](arg[2]) +end diff --git a/immortalwrt-patches/package/mtk/applications/mtwifi-cfg/files/mtwifi.sh b/immortalwrt-patches/package/mtk/applications/mtwifi-cfg/files/mtwifi.sh new file mode 100644 index 0000000..e460923 --- /dev/null +++ b/immortalwrt-patches/package/mtk/applications/mtwifi-cfg/files/mtwifi.sh @@ -0,0 +1,98 @@ +#!/bin/sh +# +# Copyright (C) 2023, hanwckf +# +# v21 2026-07-21: HONEST labels (UCI now matches physical reality). +# Driver binds profiles by MacAddress match, NOT by l1profile order: +# b0.dat (MacAddress=2.4G eeprom MAC) -> rax0 netdev (2.4G HW) +# b1.dat (MacAddress=5G eeprom MAC) -> ra0 netdev (5G HW) +# v21 additionally swaps INDEX0_profile_path in l1profile.dat to +# "b1.dat;b0.dat", so l1dat_parser maps: idx=1 (ra0) -> b1.dat, +# idx=2 (rax0) -> b0.dat. Result: each UCI section writes the DAT that +# the driver actually binds to its phy, so UCI band/ssid can be honest: +# idx=1 (ra0, 5G HW): band=5g HE160 ch 36 ssid=ImmortalWrt-5G +# idx=2 (rax0, 2.4G HW): band=2g HE40 ch auto ssid=ImmortalWrt-2.4G +# Kernel DBDC merge quirk (unchanged): VHT_BW / VHT_Sec80_Channel for +# ra0's slot are taken from b0.dat, so mtwifi_cfg still forces +# VHT_BW=2 + VHT_Sec80_Channel=155 into the band==2g profile. +# Validated live on v20 2026-07-21: ra0 2401Mbps HE160 ch36, +# rax0 HE40, DBDC_card0.dat identical to v20 working state. + +append DRIVERS "mtwifi" + +detect_mtwifi() { + local idx ifname + local band hwmode htmode htbsscoex ssid dbdc_main channel + if [ -d "/sys/module/mt_wifi" ]; then + dev_list="$(l1util list)" + for dev in $dev_list; do + ifname="$(l1util get ${dev} main_ifname)" + idx="$(l1util get ${dev} subidx)" + if [ $idx -eq 1 ]; then + # v29: UCI idx=1 is the 5G config. Driver maps band=5g content + # to the 5G PHY (whichever netdev it is this boot). + dbdc_main="1" + band="5g" + hwmode="11a" + htmode="HE160" + htbsscoex="0" + ssid="ImmortalWrt-5G" + txpower="100" + channel="36" + else + # v29: UCI idx=2 is the 2.4G config. + dbdc_main="0" + band="2g" + hwmode="11g" + htmode="HE40" + htbsscoex="1" + ssid="ImmortalWrt-2.4G" + txpower="100" + channel="auto" + fi + config_get type ${dev//./_} type + if [ "$type" != "mtwifi" ]; then + uci -q batch <<-EOF + set wireless.${dev//./_}=wifi-device + set wireless.${dev//./_}.type=mtwifi + set wireless.${dev//./_}.phy=${ifname} + set wireless.${dev//./_}.hwmode=${hwmode} + set wireless.${dev//./_}.band=${band} + set wireless.${dev//./_}.dbdc_main=${dbdc_main} + set wireless.${dev//./_}.channel=${channel} + set wireless.${dev//./_}.txpower=${txpower} + set wireless.${dev//./_}.htmode=${htmode} + set wireless.${dev//./_}.country=CN + set wireless.${dev//./_}.mu_beamformer=1 + set wireless.${dev//./_}.noscan=${htbsscoex} + set wireless.${dev//./_}.serialize=1 + + set wireless.default_${dev//./_}=wifi-iface + set wireless.default_${dev//./_}.device=${dev//./_} + set wireless.default_${dev//./_}.network=lan + set wireless.default_${dev//./_}.mode=ap + set wireless.default_${dev//./_}.ssid=${ssid} + set wireless.default_${dev//./_}.encryption=none + set wireless.default_${dev//./_}.uapsd=0 +EOF + else + uci -q batch <<-EOF + set wireless.${dev//./_}.phy=${ifname} + set wireless.${dev//./_}.hwmode=${hwmode} + set wireless.${dev//./_}.band=${band} + set wireless.${dev//./_}.dbdc_main=${dbdc_main} + set wireless.${dev//./_}.channel=${channel} + set wireless.${dev//./_}.htmode=${htmode} + set wireless.${dev//./_}.country=CN + set wireless.${dev//./_}.mu_beamformer=1 + set wireless.${dev//./_}.noscan=${htbsscoex} + set wireless.${dev//./_}.serialize=1 + set wireless.default_${dev//./_}.device=${dev//./_} + set wireless.default_${dev//./_}.network=lan + set wireless.default_${dev//./_}.mode=ap +EOF + fi + uci -q commit wireless + done + fi +} diff --git a/immortalwrt-patches/package/mtk/applications/mtwifi-cfg/files/netifd/mtwifi.sh b/immortalwrt-patches/package/mtk/applications/mtwifi-cfg/files/netifd/mtwifi.sh new file mode 100644 index 0000000..39ad9b5 --- /dev/null +++ b/immortalwrt-patches/package/mtk/applications/mtwifi-cfg/files/netifd/mtwifi.sh @@ -0,0 +1,194 @@ +#!/bin/sh +# +# Copyright (c) 2023, hanwckf +# + +. /lib/netifd/netifd-wireless.sh + +init_wireless_driver "$@" + +LOCK_FILE="/tmp/mtwifi.lock" + +MTWIFI_MAX_AP_IDX=15 +MTWIFI_MAX_APCLI_IDX=0 +MTWIFI_CFG_IFNAME_KEY="mtwifi_ifname" + +drv_mtwifi_init_device_config() { + config_add_int txpower beacon_int dtim_period + config_add_boolean mu_beamformer dbdc_main whnat bandsteering + config_add_string country twt +} + +drv_mtwifi_init_iface_config() { + config_add_string 'ssid:string' macfilter bssid kicklow assocthres steeringthresold + config_add_boolean wmm hidden isolate ieee80211k ieee80211r + config_add_int wpa_group_rekey frag rts + config_add_array 'maclist:list(macaddr)' 'steeringbssid:list(macaddr)' + config_add_boolean mumimo_dl mumimo_ul ofdma_dl ofdma_ul amsdu autoba uapsd +} + +drv_mtwifi_cleanup() { + return +} + +mtwifi_vif_ap_config() { + local name="$1" + local ifname="" + local disabled="" + + json_select config + json_get_var disabled disabled + json_select .. + + [ "$disabled" = "1" ] && return + + json_get_var ifname $MTWIFI_CFG_IFNAME_KEY + [ -z "$ifname" ] && return + + # v33: record vif for the deferred physical remap (see + # mtwifi_deferred_phys_remap). The ra*/rax* <-> PHY binding is random + # per boot and the second netdev may not exist yet at this point, so + # the remap runs after setup completes. + echo "$name $ifname" >> "/tmp/mtwifi_vifs.$MTWIFI_CUR_DEV" + + logger -t "netifd-mtwifi" "add $ifname to vifidx $name" + wireless_add_vif "$name" "$ifname" +} + +mtwifi_phys_ifname() { + # Echo the netdev that PHYSICALLY carries the given device's content. + # The section's dat (l1profile profile_path) always programs its own + # PHY, and the dat's MacAddress equals the MAC of whichever netdev + # physically carries it this boot. + local dat mac n + dat=$(l1util get "${1//_/.}" profile_path 2>/dev/null) + [ -f "$dat" ] || return + mac=$(sed -n 's/^MacAddress=//p' "$dat" | head -1) + [ -n "$mac" ] || return + for n in /sys/class/net/ra* /sys/class/net/rax*; do + [ -f "$n/address" ] || continue + if [ "$(cat "$n/address")" = "$mac" ]; then + basename "$n" + return + fi + done +} + +mtwifi_deferred_phys_remap() { + # Re-notify netifd with the physical ifname for each vif of device $1 + # once the netdev shows up, so ubus status (and everything derived + # from it: LuCI title, freqlist, channel dropdown, bridge membership) + # matches reality. UCI writes stay section-based and unaffected. + local dev="$1" vifs="/tmp/mtwifi_vifs.$1" + local name static_if phys tries=0 + [ -s "$vifs" ] || return + while [ $tries -lt 45 ]; do + phys=$(mtwifi_phys_ifname "$dev") + [ -n "$phys" ] && break + sleep 1 + tries=$((tries+1)) + done + [ -n "$phys" ] || return + while read -r name static_if; do + [ -n "$name" ] || continue + [ "$phys" = "$static_if" ] && continue + logger -t "netifd-mtwifi" "deferred physical remap: $dev $static_if -> $phys" + __netifd_device="$dev" + wireless_add_vif "$name" "$phys" + done < "$vifs" + rm -f "$vifs" +} + +mtwifi_vif_sta_config() { + local name="$1" + local ifname="" + local disabled="" + + json_select config + json_get_var disabled disabled + json_select .. + + [ "$disabled" = "1" ] && return + + json_get_var ifname $MTWIFI_CFG_IFNAME_KEY + + if [ -n "$ifname" ]; then + logger -t "netifd-mtwifi" "add $ifname to vifidx $name" + wireless_add_vif "$name" "$ifname" + fi +} + +mtwifi_vif_ap_set_data() { + local ifname="" + + if [ ! $AP_IDX -gt $MTWIFI_MAX_AP_IDX ]; then + ifname="${MTWIFI_AP_IF_PREFIX}${AP_IDX}" + AP_IDX=$((AP_IDX+1)) + fi + + json_add_string "$MTWIFI_CFG_IFNAME_KEY" "$ifname" +} + +mtwifi_vif_sta_set_data() { + local ifname="" + + if [ ! $APCLI_IDX -gt $MTWIFI_MAX_APCLI_IDX ]; then + ifname="${MTWIFI_APCLI_IF_PREFIX}${APCLI_IDX}" + APCLI_IDX=$((APCLI_IDX+1)) + fi + + json_add_string "$MTWIFI_CFG_IFNAME_KEY" "$ifname" +} + +drv_mtwifi_setup() { + ubus -t 120 wait_for network.interface.lan + + local dev="$1" + # UCI section names use underscores (e.g., MT7981_1_1) but l1util/l1dat + # expect dotted names (e.g., MT7981.1.1). Convert before any lookup. + local dev_dot="${dev//_/.}" + + json_add_string device "$dev_dot" + MTWIFI_CUR_DEV="$dev" + rm -f "/tmp/mtwifi_vifs.$dev" + + lock $LOCK_FILE + + logger -t "netifd-mtwifi" "up: $dev ($dev_dot)" + + MTWIFI_AP_IF_PREFIX="$(l1util get $dev_dot ext_ifname)" + MTWIFI_APCLI_IF_PREFIX="$(l1util get $dev_dot apcli_ifname)" + + AP_IDX=0 + for_each_interface ap mtwifi_vif_ap_set_data + + APCLI_IDX=0 + for_each_interface sta mtwifi_vif_sta_set_data + + json_dump | /sbin/mtwifi_cfg setup + + for_each_interface ap mtwifi_vif_ap_config + for_each_interface sta mtwifi_vif_sta_config + + wireless_set_up + + lock -u $LOCK_FILE + + ( mtwifi_deferred_phys_remap "$dev" ) >/dev/null 2>&1