feat: band-aware channel pickers for Open AP and Evil WPA

This commit is contained in:
2026-08-18 07:26:55 -05:00
parent 4fa7f8f855
commit ff3bd16855
@@ -410,10 +410,53 @@ views.pineap = (root) => {
return { destroy: () => clearInterval(iv) }; return { destroy: () => clearInterval(iv) };
}; };
const OPEN_CHANNELS = Array.from({ length: 11 }, (_, i) => { const BAND_GROUPS = [
const c = i + 1; { band: '2.4', label: '2.4 GHz', dfs: false,
return [c, 'Channel ' + c + ' (' + (2412 + (c - 1) * 5) + ' MHz)']; channels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] },
}); { band: '5', label: '5 GHz', dfs: true,
channels: [36, 40, 44, 48, 52, 56, 60, 64,
100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144,
149, 153, 157, 161, 165] },
{ band: '6', label: '6 GHz (WPA3/OWE only)', dfs: false,
channels: Array.from({ length: 14 }, (_, i) => 181 + i * 4) }
];
const DFS_SET = new Set([52, 56, 60, 64, 100, 104, 108, 112, 116, 120,
124, 128, 132, 136, 140, 144]);
function chanFreq(band, ch) {
if (band === '2.4') return 2412 + (ch - 1) * 5;
if (band === '5') return 5180 + (ch - 36) * 5;
return 5955 + (ch - 1) * 5; // 6 GHz
}
function chanLabel(band, ch) {
return 'Channel ' + ch + ' (' + chanFreq(band, ch) + ' MHz)'
+ (band === '5' && DFS_SET.has(ch) ? ' (DFS)' : '');
}
function chanSelect(sel, value) {
BAND_GROUPS.forEach((g) => {
const og = h('optgroup', { label: g.label });
g.channels.forEach((ch) => {
og.appendChild(h('option', { value: ch, text: chanLabel(g.band, ch) }));
});
sel.appendChild(og);
});
if (value != null) {
const opts = Array.prototype.slice.call(sel.options);
const hit = opts.find((o) => Number(o.value) === Number(value));
if (hit) sel.value = hit.value;
}
return sel;
}
function bandOfChannel(ch) {
if (ch == null) return '2.4';
ch = Number(ch);
if (ch >= 1 && ch <= 14) return '2.4';
if (ch >= 36 && ch <= 177) return '5';
return '6';
}
const OPEN_COUNTRIES = [ const OPEN_COUNTRIES = [
['US', 'United States'], ['DZ', 'Algeria'], ['AR', 'Argentina'], ['AU', 'Australia'], ['US', 'United States'], ['DZ', 'Algeria'], ['AR', 'Argentina'], ['AU', 'Australia'],
['AT', 'Austria'], ['BH', 'Bahrain'], ['BM', 'Bermuda'], ['BO', 'Bolivia'], ['BR', 'Brazil'], ['AT', 'Austria'], ['BH', 'Bahrain'], ['BM', 'Bermuda'], ['BO', 'Bolivia'], ['BR', 'Brazil'],
@@ -448,7 +491,12 @@ views.pineap_open = (root) => {
const ssidIn = h('input', { id: 'oa-ssid' }); const ssidIn = h('input', { id: 'oa-ssid' });
const bssidIn = h('input', { id: 'oa-bssid' }); const bssidIn = h('input', { id: 'oa-bssid' });
const chSel = h('select', { id: 'oa-channel' }); const chSel = h('select', { id: 'oa-channel' });
OPEN_CHANNELS.forEach(([v, l]) => chSel.appendChild(h('option', { value: v, text: l }))); chanSelect(chSel, null);
const bandHint = h('div', { class: 'muted', style: 'font-size:12px;margin-top:4px' });
chSel.addEventListener('change', () => {
const b = bandOfChannel(chSel.value);
bandHint.textContent = b === '6' ? '6 GHz open APs require WPA3/OWE on real clients — most devices will not associate to an open 6 GHz network.' : '';
});
const coSel = h('select', { id: 'oa-country' }); const coSel = h('select', { id: 'oa-country' });
OPEN_COUNTRIES.forEach(([v, l]) => coSel.appendChild(h('option', { value: v, text: l }))); OPEN_COUNTRIES.forEach(([v, l]) => coSel.appendChild(h('option', { value: v, text: l })));
const hiddenCb = h('input', { type: 'checkbox', id: 'oa-hidden' }); const hiddenCb = h('input', { type: 'checkbox', id: 'oa-hidden' });
@@ -464,7 +512,7 @@ views.pineap_open = (root) => {
h('div', {}, h('label', {}, 'Open SSID', ssidIn)), h('div', {}, h('label', {}, 'Open SSID', ssidIn)),
h('div', {}, h('label', {}, 'BSSID', bssidIn)))); h('div', {}, h('label', {}, 'BSSID', bssidIn))));
card.appendChild(h('div', { class: 'row' }, card.appendChild(h('div', { class: 'row' },
h('div', {}, h('label', {}, 'Channel', chSel)), h('div', {}, h('label', {}, 'Channel', chSel), bandHint),
h('div', {}, h('label', {}, 'Current Country', coSel)))); h('div', {}, h('label', {}, 'Current Country', coSel))));
card.appendChild(h('div', { class: 'row' }, card.appendChild(h('div', { class: 'row' },
h('div', {}, h('label', { class: 'switch' }, hiddenCb, h('span', { class: 'track' }), ' Hidden')), h('div', {}, h('label', { class: 'switch' }, hiddenCb, h('span', { class: 'track' }), ' Hidden')),
@@ -579,7 +627,12 @@ views.pineap_open = (root) => {
const open = a.open || {}; const open = a.open || {};
ssidIn.value = open.ssid || ''; ssidIn.value = open.ssid || '';
bssidIn.value = open.bssid || ''; bssidIn.value = open.bssid || '';
if (open.channel != null) chSel.value = String(open.channel); if (open.channel != null) {
const opts = Array.prototype.slice.call(chSel.options);
if (opts.some((o) => Number(o.value) === Number(open.channel))) {
chSel.value = String(open.channel);
}
}
if (open.country) coSel.value = open.country; if (open.country) coSel.value = open.country;
hiddenCb.checked = !!open.hidden; hiddenCb.checked = !!open.hidden;
state.enabledLoaded = !!(a.open); state.enabledLoaded = !!(a.open);
@@ -614,16 +667,29 @@ views.pineap_evilwpa = (root) => {
EVIL_ENC.forEach(([v, l]) => encSel.appendChild(h('option', { value: v, text: l }))); EVIL_ENC.forEach(([v, l]) => encSel.appendChild(h('option', { value: v, text: l })));
const hiddenCb = h('input', { type: 'checkbox', id: 'ew-hidden' }); const hiddenCb = h('input', { type: 'checkbox', id: 'ew-hidden' });
const enabledCb = h('input', { type: 'checkbox', id: 'ew-enabled' }); const enabledCb = h('input', { type: 'checkbox', id: 'ew-enabled' });
const wpaChan = h('select', { id: 'ew-channel' });
chanSelect(wpaChan, null);
const wpaHint = h('div', { class: 'muted', style: 'font-size:12px;margin-top:4px' });
wpaChan.addEventListener('change', () => {
const b = bandOfChannel(wpaChan.value);
const six = b === '6';
Array.prototype.forEach.call(encSel.options, (o) => { o.disabled = six && o.value === 'psk2'; });
if (six && encSel.value === 'psk2') encSel.value = 'sae';
wpaHint.textContent = six ? '6 GHz requires WPA3 (SAE or OWE).' : '';
});
cfg.appendChild(h('label', {}, 'SSID', ssidIn)); cfg.appendChild(h('label', {}, 'SSID', ssidIn));
cfg.appendChild(h('label', {}, 'Passphrase', pskIn)); cfg.appendChild(h('label', {}, 'Passphrase', pskIn));
cfg.appendChild(h('label', {}, 'Encryption', encSel)); cfg.appendChild(h('label', {}, 'Encryption', encSel));
cfg.appendChild(h('label', {}, 'Channel', wpaChan));
cfg.appendChild(wpaHint);
cfg.appendChild(h('label', { class: 'switch' }, hiddenCb, h('span', { class: 'track' }), 'Hidden')); cfg.appendChild(h('label', { class: 'switch' }, hiddenCb, h('span', { class: 'track' }), 'Hidden'));
cfg.appendChild(h('label', { class: 'switch' }, enabledCb, h('span', { class: 'track' }), 'Enabled')); cfg.appendChild(h('label', { class: 'switch' }, enabledCb, h('span', { class: 'track' }), 'Enabled'));
cfg.appendChild(h('div', { class: 'row' }, cfg.appendChild(h('div', { class: 'row' },
h('div', {}, btn('Save', () => { h('div', {}, btn('Save', () => {
PagerAPI.post('/api/pineap/wifi/set_ap', { PagerAPI.post('/api/pineap/wifi/set_ap', {
wpa: { ssid: ssidIn.value, passphrase: pskIn.value, enctype: encSel.value, wpa: { ssid: ssidIn.value, passphrase: pskIn.value, enctype: encSel.value,
hidden: hiddenCb.checked, enabled: enabledCb.checked } hidden: hiddenCb.checked, enabled: enabledCb.checked,
channel: wpaChan.value ? parseInt(wpaChan.value, 10) : 1 }
}).then(() => { App.toast('Evil WPA saved'); load(); }).catch(() => App.toast('Failed', 'error')); }).then(() => { App.toast('Evil WPA saved'); load(); }).catch(() => App.toast('Failed', 'error'));
})), })),
h('div', { class: 'muted', style: 'align-self:center;font-size:12px' }, 'Applying reconfigures the radio — you may be disconnected briefly.'))); h('div', { class: 'muted', style: 'align-self:center;font-size:12px' }, 'Applying reconfigures the radio — you may be disconnected briefly.')));
@@ -675,6 +741,12 @@ views.pineap_evilwpa = (root) => {
} }
hiddenCb.checked = !!w.hidden; hiddenCb.checked = !!w.hidden;
enabledCb.checked = !!w.enabled; enabledCb.checked = !!w.enabled;
if (w.channel != null) {
const opts = Array.prototype.slice.call(wpaChan.options);
if (opts.some((o) => Number(o.value) === Number(w.channel))) {
wpaChan.value = String(w.channel);
}
}
}).catch(() => {}); }).catch(() => {});
PagerAPI.get('/api/pineap/get_config').then((r) => { PagerAPI.get('/api/pineap/get_config').then((r) => {
const p = r.data || {}; const p = r.data || {};