diff --git a/ajax/networking/get_channel.php b/ajax/networking/get_channel.php index 716456c4..e158588e 100644 --- a/ajax/networking/get_channel.php +++ b/ajax/networking/get_channel.php @@ -13,7 +13,9 @@ foreach ($hostapdconfig as $hostapdconfigline) { continue; } $arrLine = explode("=", $hostapdconfigline); - $arrConfig[$arrLine[0]]=$arrLine[1]; + if (count($arrLine) >= 2) { + $arrConfig[$arrLine[0]]=$arrLine[1]; + } }; $channel = intval($arrConfig['channel']); echo json_encode($channel); diff --git a/app/img/wifi-qr-code.php b/app/img/wifi-qr-code.php index 19171094..a0017c3a 100755 --- a/app/img/wifi-qr-code.php +++ b/app/img/wifi-qr-code.php @@ -12,6 +12,12 @@ if (!isset($_SERVER['HTTP_REFERER'])) { $hostapd = parse_ini_file(RASPI_HOSTAPD_CONFIG, false, INI_SCANNER_RAW); +// handle parse failure +if ($hostapd === false) { + header('HTTP/1.0 500 Internal Server Error'); + exit('Error: Unable to parse hostapd configuration'); +} + // assume WPA encryption and get the passphrase $type = "WPA"; $password = isset($hostapd['wpa_psk']) ? $hostapd['wpa_psk'] : $hostapd['wpa_passphrase']; diff --git a/app/js/ajax/main.js b/app/js/ajax/main.js index f8bf997a..ce9edf6b 100644 --- a/app/js/ajax/main.js +++ b/app/js/ajax/main.js @@ -381,12 +381,15 @@ function loadChannelSelect(selected) { // Map selected hw_mode to available channels if (hw_mode === 'a') { selectableChannels = data.filter(item => item.MHz.toString().startsWith('5')); - } else if (hw_mode !== 'ac') { - selectableChannels = data.filter(item => item.MHz.toString().startsWith('24')); - } else if (hw_mode === 'b') { - selectableChannels = data.filter(item => item.MHz.toString().startsWith('24')); } else if (hw_mode === 'ac') { selectableChannels = data.filter(item => item.MHz.toString().startsWith('5')); + } else if (hw_mode === 'ax') { + selectableChannels = data.filter(item => item.MHz.toString().startsWith('5')); + } else if (hw_mode === 'be') { + selectableChannels = data.filter(item => item.MHz.toString().startsWith('5')); + } else { + // hw_mode 'b', 'g', or default to 2.4GHz + selectableChannels = data.filter(item => item.MHz.toString().startsWith('24')); } // If selected channel doeesn't exist in allowed channels, set default or null (unsupported) diff --git a/config/defaults.json b/config/defaults.json index c9770cc1..a586eebd 100644 --- a/config/defaults.json +++ b/config/defaults.json @@ -14,7 +14,7 @@ "# N", "ieee80211n=1", "require_ht=1", - "ht_capab=[MAX-AMSDU-3839][HT40+][SHORT-GI-20][SHORT-GI-40][DSSS_CCK-40]", + "ht_capab=[MAX-AMSDU-3839][{HT40_DIR}][SHORT-GI-20][SHORT-GI-40][DSSS_CCK-40]", "# AC", "ieee80211ac=1", "require_vht=1", @@ -25,6 +25,148 @@ "vht_oper_centr_freq_seg0_idx={VHT_FREQ_IDX}" ] }, + "ax": { + "settings": [ + "# Basic settings", + "hw_mode=a", + "# Enable 802.11n/ac", + "ieee80211d=1", + "ieee80211n=1", + "ieee80211ac=1", + "# Enable 802.11ax", + "ieee80211ax=1", + "# HE 802.11ax capabilities", + "he_su_beamformer=1", + "he_su_beamformee=1", + "he_mu_beamformer=1", + "# BSS color for spatial reuse, value 1-63", + "he_bss_color=1", + "he_oper_chwidth=1", + "# HE/VHT channel widths", + "he_oper_chwidth=1", + "vht_oper_chwidth=1", + "he_oper_centr_freq_seg0_idx={HE_FREQ_IDX}", + "vht_oper_centr_freq_seg0_idx={VHT_FREQ_IDX}", + "# HT 802.11n capabilities", + "ht_capab=[{HT40_DIR}][LDPC][SHORT-GI-20][SHORT-GI-40][TX-STBC][RX-STBC1][MAX-AMSDU-7935]", + "# VHT capabilities 802.11ac", + "vht_capab=[RXLDPC][SHORT-GI-80][TX-STBC-2BY1][RX-STBC-1][MAX-MPDU-11454][MAX-A-MPDU-LEN-EXP7]", + "# WMM/QoS", + "wmm_enabled=1" + ] + }, + "be": { + "settings": [ + "# Basic settings", + "hw_mode=a", + "# Enable 802.11n/ac/ax", + "ieee80211n=1", + "ieee80211ac=1", + "ieee80211ax=1", + "# Maximum MPDU Length of HE 6 GHz band capabilities.", + "# Indicates maximum MPDU length", + "# 0 = 3895 octets", + "# 1 = 7991 octets", + "# 2 = 11454 octets", + "he_6ghz_max_mpdu=2", + "# Maximum A-MPDU Length Exponent of HE 6 GHz band capabilities. Indicates", + "# the maximum length of A-MPDU pre-EOF padding that # the STA can receive.", + "# This field is an integer in the range of 0 to 7. The length defined by", + "# this field is equal to 2 pow -1", + "# octets", + "# 0 = AMPDU length of 8k", + "# 1 = AMPDU length of 16k", + "# 2 = AMPDU length of 32k", + "# 3 = AMPDU length of 65k", + "# 4 = AMPDU length of 131k", + "# 5 = AMPDU length of 262kv", + "# 6 = AMPDU length of 524k", + "# 7 = AMPDU length of 1048k", + "he_6ghz_max_ampdu_len_exp=7", + "# 0 = Indoor AP", + "# 1 = Standard power AP", + "# 2 = Very low power AP", + "# 3 = Indoor enabled AP", + "# 4 = Indoor standard power AP", + "he_6ghz_reg_pwr_type=0", + "# HE beamforming capabilities", + "he_su_beamformer=1", + "he_su_beamformee=1", + "he_mu_beamformer=1", + "he_mu_edca_qos_info_param_count=0", + "he_mu_edca_qos_info_q_ack=0", + "he_mu_edca_qos_info_queue_request=0", + "he_mu_edca_qos_info_txop_request=0", + "he_mu_edca_ac_be_aifsn=8", + "he_mu_edca_ac_be_aci=0", + "he_mu_edca_ac_be_ecwmin=9", + "he_mu_edca_ac_be_ecwmax=10", + "he_mu_edca_ac_be_timer=255", + "he_mu_edca_ac_bk_aifsn=15", + "he_mu_edca_ac_bk_aci=1", + "he_mu_edca_ac_bk_ecwmin=9", + "he_mu_edca_ac_bk_ecwmax=10", + "he_mu_edca_ac_bk_timer=255", + "he_mu_edca_ac_vi_ecwmin=5", + "he_mu_edca_ac_vi_ecwmax=7", + "he_mu_edca_ac_vi_aifsn=5", + "he_mu_edca_ac_vi_aci=2", + "he_mu_edca_ac_vi_timer=255", + "he_mu_edca_ac_vo_aifsn=5", + "he_mu_edca_ac_vo_aci=3", + "he_mu_edca_ac_vo_ecwmin=5", + "he_mu_edca_ac_vo_ecwmax=7", + "he_mu_edca_ac_vo_timer=255", + "# EHT beamforming capabilities", + "eht_su_beamformer=0", + "eht_su_beamformee=0", + "eht_mu_beamformer=0", + "# used by clients to discern the source of interference", + "# each AP in your area needs to use a different number", + "# allowed: 1-63", + "he_bss_color=37", + "# 160 MHz for HE", + "he_oper_chwidth=2", + "he_oper_centr_freq_seg0_idx={HE_FREQ_IDX}", + "# IEEE 802.11be WiFi 7 configuration", + "ieee80211be=1", + "# EHT configuration", + "eht_su_beamformer=1", + "eht_su_beamformee=1", + "eht_mu_beamformer=1", + "# EHT operating channel information; see matching he_* parameters for details.", + "# The field eht_oper_centr_freq_seg0_idx field is used to indicate center", + "# frequency of 40, 80, and 160 MHz bandwidth operation.", + "# In the 6 GHz band, eht_oper_chwidth is ignored and the channel width is", + "# derived from the configured operating class IEEE P802.11be/D1.5,", + "# Annex E.1 - Country information and operating classes.", + "# Channel width 0 = 40 MHz, 1 = 80 Mhz, 2 = 160 Mhz", + "eht_oper_chwidth=2", + "eht_oper_centr_freq_seg0_idx={VHT_FREQ_IDX}", + "# VHT operation parameters", + "vht_oper_chwidth=2", + "vht_oper_centr_freq_seg0_idx={VHT_FREQ_IDX}", + "vht_capab=[MAX-MPDU-11454][RXLDPC][SHORT-GI-80][SHORT-GI-160][TX-STBC-2BY1][RX-STBC-1][SU-BEAMFORMER][SU-BEAMFORMEE][MU-BEAMFORMER][MU-BEAMFORMEE]", + "# WMM configuration", + "wmm_enabled=1", + "wmm_ac_bk_cwmin=4", + "wmm_ac_bk_cwmax=10", + "wmm_ac_bk_aifs=7", + "wmm_ac_bk_txop_limit=0", + "wmm_ac_be_aifs=3", + "wmm_ac_be_cwmin=4", + "wmm_ac_be_cwmax=10", + "wmm_ac_be_txop_limit=0", + "wmm_ac_vi_aifs=2", + "wmm_ac_vi_cwmin=3", + "wmm_ac_vi_cwmax=4", + "wmm_ac_vi_txop_limit=94", + "wmm_ac_vo_aifs=2", + "wmm_ac_vo_cwmin=2", + "wmm_ac_vo_cwmax=3", + "wmm_ac_vo_txop_limit=47" + ] + }, "g": { "settings": [ "hw_mode=g", diff --git a/src/RaspAP/Networking/Hotspot/DhcpcdManager.php b/src/RaspAP/Networking/Hotspot/DhcpcdManager.php index 7d5ad084..f4c0052a 100644 --- a/src/RaspAP/Networking/Hotspot/DhcpcdManager.php +++ b/src/RaspAP/Networking/Hotspot/DhcpcdManager.php @@ -131,15 +131,15 @@ class DhcpcdManager $dhcp_cfg = $this->removeIface($dhcp_cfg,'uap0'); $dhcp_cfg .= $config; } else { - if (strpos($dhcp_cfg, 'interface '.$ap_iface) !== false && - strpos($dhcp_cfg, 'nogateway') !== false) { - $config[] = 'nogateway'; - } $config = join(PHP_EOL, $config); $dhcp_cfg = $this->removeIface($dhcp_cfg,'br0'); $dhcp_cfg = $this->removeIface($dhcp_cfg,'uap0'); if (!strpos($dhcp_cfg, 'metric')) { - $dhcp_cfg = preg_replace('/^#\sRaspAP\s'.$ap_iface.'\s.*?(?:\s*^\s*$|\s*nogateway)/ms', $config, $dhcp_cfg, 1); + $pattern = '/^#\sRaspAP\s' . preg_quote($ap_iface, '/') . '\sconfiguration\n' . + '(?:.*\n)*?' . + '(?:\n)*' . + '(?=#\sRaspAP\s|\z)/m'; + $dhcp_cfg = preg_replace($pattern, $config . "\n\n", $dhcp_cfg, 1); } else { $metrics = true; } @@ -199,12 +199,11 @@ class DhcpcdManager $status->addMessage('DHCP configuration for '.$iface.' added.', 'success'); } else { $cfg = join(PHP_EOL, $cfg); - $dhcp_cfg = preg_replace( - '/^#\sRaspAP\s'.$iface.'\s.*?(?=\n*(?:^#\sRaspAP|^interface\s(?!'.$iface.'$)|\z))/ms', - $cfg . PHP_EOL, - $dhcp_cfg, - 1 - ); + $pattern = '/^#\sRaspAP\s' . preg_quote($iface, '/') . '\sconfiguration\n' . + '(?:.*\n)*?' . + '(?:\n)*' . + '(?=#\sRaspAP\s|\z)/m'; + $dhcp_cfg = preg_replace($pattern, $cfg . "\n\n", $dhcp_cfg, 1); } return $dhcp_cfg; @@ -363,18 +362,29 @@ class DhcpcdManager ]; // merge existing settings with updates + $processed_keys = []; foreach ($existing_config as $line) { $matched = false; foreach ($static_settings as $key => $value) { if (strpos($line, $key) === 0) { $config[] = "$key=$value"; $matched = true; + $processed_keys[] = $key; unset($static_settings[$key]); break; } } if (!$matched && !preg_match('/^interface/', $line)) { - $config[] = $line; + $is_duplicate = false; + foreach ($processed_keys as $processed_key) { + if (strpos($line, $processed_key) === 0) { + $is_duplicate = true; + break; + } + } + if (!$is_duplicate && !in_array($line, $config, true)) { + $config[] = $line; + } } } diff --git a/src/RaspAP/Networking/Hotspot/HostapdManager.php b/src/RaspAP/Networking/Hotspot/HostapdManager.php index f0ba3874..6caf56be 100644 --- a/src/RaspAP/Networking/Hotspot/HostapdManager.php +++ b/src/RaspAP/Networking/Hotspot/HostapdManager.php @@ -102,8 +102,11 @@ class HostapdManager if (!empty($config['ieee80211ac']) && strval($config['ieee80211ac']) === '1') { $selected = 'ac'; } - if (!empty($config['ieee80211w']) && strval($config['ieee80211w']) === '2') { - $selected = 'w'; + if (!empty($config['ieee80211ax']) && strval($config['ieee80211ax']) === '1') { + $selected = 'ax'; + } + if (!empty($config['ieee80211be']) && strval($config['ieee80211be']) === '1') { + $selected = 'be'; } return $selected; @@ -151,6 +154,7 @@ class HostapdManager $config[] = 'auth_algs=1'; $wpa = $params['wpa']; + $wpa_numeric = $wpa; $wpa_key_mgmt = 'WPA-PSK'; if ($wpa == 4) { @@ -185,20 +189,42 @@ class HostapdManager $config[] = 'ssid=' . $params['ssid']; $config[] = 'channel=' . $params['channel']; - // choose VHT segment index (fallback only if required) - $vht_freq_idx = ($params['channel'] < RASPI_5GHZ_CHANNEL_MIN) ? 42 : 155; $hwMode = isset($params['hw_mode']) ? $params['hw_mode'] : ''; + // validate channel width for 802.11ax/be + if (in_array($hwMode, ['ax', 'be'])) { + // for 6GHz band (channels 1-233) wider bandwidths are available + $is6GHz = ($params['channel'] >= 1 && $params['channel'] <= 233); + + // for 802.11be, 320 MHz only available on 6GHz + if ($hwMode === 'be' && !$is6GHz && isset($params['eht_oper_chwidth']) && $params['eht_oper_chwidth'] == 4) { + // reset to 160 MHz if 320 MHz requested on non-6GHz + $params['eht_oper_chwidth'] = 2; + } + } + // fetch settings for selected mode $modeSettings = getDefaultNetOpts('hostapd', 'modes', $hwMode); $settings = $modeSettings[$hwMode]['settings'] ?? []; + // extract channel width from settings to calculate center frequency + $chwidth = $this->extractChannelWidth($settings, $hwMode); + + // calculate center frequency indices based on channel + width + $vht_freq_idx = $this->calculateCenterFreqIndex((int)$params['channel'], $chwidth); + $he_freq_idx = $vht_freq_idx; // For most cases, HE and VHT use the same center frequency + + // calculate HT40 direction based on channel and width + $ht40_dir = $this->calculateHT40Direction((int)$params['channel'], $chwidth); + if (!empty($settings)) { foreach ($settings as $line) { if (!is_string($line)) { continue; } - $replaced = str_replace('{VHT_FREQ_IDX}', (string) $vht_freq_idx ?? '',$line); + $replaced = str_replace('{VHT_FREQ_IDX}', (string) $vht_freq_idx ?? '', $line); + $replaced = str_replace('{HE_FREQ_IDX}', (string) $he_freq_idx ?? '', $replaced); + $replaced = str_replace('{HT40_DIR}', (string) $ht40_dir ?? '', $replaced); $config[] = $replaced; } } @@ -439,5 +465,241 @@ class HostapdManager return is_array($configs) ? count($configs) : 0; } -} + /** + * Extracts channel width from mode settings + * + * @param array $settings mode settings array + * @param string $hwMode hardware mode (ac, ax, be) + * @return int channel width in MHz (20, 40, 80, 160, 320) + */ + private function extractChannelWidth(array $settings, string $hwMode): int + { + $chwidthParam = ''; + // determine parameter based on mode + if ($hwMode === 'ac') { + $chwidthParam = 'vht_oper_chwidth'; + } elseif ($hwMode === 'ax') { + $chwidthParam = 'he_oper_chwidth'; + } elseif ($hwMode === 'be') { + $chwidthParam = 'eht_oper_chwidth'; + } else { + return 20; // 20 MHz default for other modes + } + + // parse settings to find channel width + foreach ($settings as $line) { + if (!is_string($line)) { + continue; + } + + // skip comments + if (strpos(trim($line), '#') === 0) { + continue; + } + + if (strpos($line, $chwidthParam . '=') !== false) { + $parts = explode('=', $line, 2); + if (count($parts) === 2) { + // extract numeric value + $value = trim($parts[1]); + // remove any inline comments + $value = preg_replace('/\s*#.*$/', '', $value); + $chwidthCode = (int) $value; + + // convert hostapd encoding to MHz based on mode + if ($hwMode === 'be') { + // EHT uses: 0=20, 1=40, 2=80, 3=160, 4=320 + switch ($chwidthCode) { + case 0: return 20; + case 1: return 40; + case 2: return 80; + case 3: return 160; + case 4: return 320; + default: return 20; + } + } else { + // VHT/HE uses: 0=20/40, 1=80, 2=160, 3=80+80 + switch ($chwidthCode) { + case 0: return 40; + case 1: return 80; + case 2: return 160; + case 3: return 160; // 80+80 treated as 160 + default: return 20; + } + } + } + } + } + + return 20; // default to 20 MHz channel if not found + } + + /** + * Calculates center frequency segment 0 index for given channel and width + * + * @param int $channel primary channel number + * @param int $chwidthMHz channel width in MHz (20, 40, 80, 160, 320) + * @return int center frequency segment 0 index + */ + private function calculateCenterFreqIndex(int $channel, int $chwidthMHz): int + { + // determine band based on channel number + $is24GHz = ($channel >= 1 && $channel <= 14); + $is5GHz = ($channel >= 36 && $channel <= 177); + $is6GHz = ($channel >= 1 && $channel <= 233 && !$is24GHz); // 6 GHz uses 1-233 + + // 20 MHz - center is always primary channel + if ($chwidthMHz <= 20) { + return $channel; + } + + // 2.4 GHz band + if ($is24GHz) { + if ($chwidthMHz == 40) { + // for 2.4 GHz, typically use HT40+ (center is primary + 2) + // channels 1-7 use HT40+, channels 8-13 use HT40- + return ($channel <= 7) ? $channel + 2 : $channel - 2; + } + // wider bandwidths not supported on 2.4 GHz + return $channel; + } + + // 5 GHz band + if ($is5GHz) { + if ($chwidthMHz == 40) { + // HT40+ configuration: center = primary + 2 + // adjust for upper/lower position in 40 MHz pair + if (in_array($channel, [36, 44, 52, 60, 100, 108, 116, 124, 132, 140, 149, 157, 165, 173])) { + return $channel + 2; + } else { + return $channel - 2; + } + } + + if ($chwidthMHz == 80) { + // map channel to 80 MHz center frequency + if ($channel >= 36 && $channel <= 48) return 42; + if ($channel >= 52 && $channel <= 64) return 58; + if ($channel >= 100 && $channel <= 112) return 106; + if ($channel >= 116 && $channel <= 128) return 122; + if ($channel >= 132 && $channel <= 144) return 138; + if ($channel >= 149 && $channel <= 161) return 155; + if ($channel >= 165 && $channel <= 177) return 171; + } + + if ($chwidthMHz == 160) { + // map channel to 160 MHz center frequency + if ($channel >= 36 && $channel <= 64) return 50; + if ($channel >= 100 && $channel <= 128) return 114; + // channels 149-177 don't support 160 MHz in most regions + if ($channel >= 149 && $channel <= 177) return 163; + } + } + + // 6 GHz band (UNII-5 through UNII-8) + if ($is6GHz && !$is24GHz) { + // 6 GHz uses different channel numbering: 1, 5, 9, 13, ... (every 4) + if ($chwidthMHz == 40) { + // center is at the midpoint between two 20 MHz channels + return $channel + 2; + } + + if ($chwidthMHz == 80) { + // calculate 80 MHz center + $blockStart = (int)(($channel - 1) / 16) * 16 + 1; + return $blockStart + 6; + } + + if ($chwidthMHz == 160) { + // calculate 160 MHz center + $blockStart = (int)(($channel - 1) / 32) * 32 + 1; + return $blockStart + 14; + } + + if ($chwidthMHz == 320) { + // calculate 320 MHz center + $blockStart = (int)(($channel - 1) / 64) * 64 + 1; + return $blockStart + 30; + } + } + + // fallback: return primary channel + return $channel; + } + + /** + * Calculates HT40 direction (+ or -) based on channel and bandwidth + * + * @param int $channel primary channel number + * @param int $chwidthMHz channel width in MHz + * @return string HT40 direction: "HT40+" or "HT40-" or "" for 20MHz + */ + private function calculateHT40Direction(int $channel, int $chwidthMHz): string + { + // only applicable for 40 MHz and wider on 5 GHz + if ($chwidthMHz < 40) { + return ''; + } + + $is24GHz = ($channel >= 1 && $channel <= 14); + $is5GHz = ($channel >= 36 && $channel <= 177); + + // 2.4 GHz band + if ($is24GHz) { + // channels 1-7 use HT40+, channels 8-13 use HT40- + return ($channel <= 7) ? 'HT40+' : 'HT40-'; + } + + // 5 GHz band + if ($is5GHz) { + if ($chwidthMHz == 40) { + // for pure 40 MHz mode + if (in_array($channel, [36, 44, 52, 60, 100, 108, 116, 124, 132, 140, 149, 157, 165, 173])) { + return 'HT40+'; + } else { + return 'HT40-'; + } + } + + if ($chwidthMHz >= 80) { + // for 80 MHz and wider, determine based on position within the 80 MHz block + // lower half of 80 MHz block uses HT40+, upper half uses HT40- + + // determine which 80 MHz block this channel belongs to + if ($channel >= 36 && $channel <= 48) { + // block: 36, 40, 44, 48 (center 42) + return ($channel <= 40) ? 'HT40+' : 'HT40-'; + } + if ($channel >= 52 && $channel <= 64) { + // block: 52, 56, 60, 64 (center 58) + return ($channel <= 56) ? 'HT40+' : 'HT40-'; + } + if ($channel >= 100 && $channel <= 112) { + // block: 100, 104, 108, 112 (center 106) + return ($channel <= 104) ? 'HT40+' : 'HT40-'; + } + if ($channel >= 116 && $channel <= 128) { + // block: 116, 120, 124, 128 (center 122) + return ($channel <= 120) ? 'HT40+' : 'HT40-'; + } + if ($channel >= 132 && $channel <= 144) { + // block: 132, 136, 140, 144 (center 138) + return ($channel <= 136) ? 'HT40+' : 'HT40-'; + } + if ($channel >= 149 && $channel <= 161) { + // block: 149, 153, 157, 161 (center 155) + return ($channel <= 153) ? 'HT40+' : 'HT40-'; + } + if ($channel >= 165 && $channel <= 177) { + // block: 165, 169, 173, 177 (center 171) + return ($channel <= 169) ? 'HT40+' : 'HT40-'; + } + } + } + + // default fallback + return 'HT40+'; + } + +} diff --git a/src/RaspAP/Networking/Hotspot/HotspotService.php b/src/RaspAP/Networking/Hotspot/HotspotService.php index 10e2f912..8322cd1c 100644 --- a/src/RaspAP/Networking/Hotspot/HotspotService.php +++ b/src/RaspAP/Networking/Hotspot/HotspotService.php @@ -32,7 +32,9 @@ class HotspotService 'b' => '802.11b - 2.4 GHz', 'g' => '802.11g - 2.4 GHz', 'n' => '802.11n - 2.4/5 GHz', - 'ac' => '802.11ac - 5 GHz' + 'ac' => '802.11ac - 5 GHz', + 'ax' => '802.11ax - 2.4/5/6 GHz', + 'be' => '802.11be - 2.4/5/6 GHz' ]; // encryption types @@ -42,6 +44,21 @@ class HotspotService 'TKIP CCMP' => 'TKIP+CCMP' ]; + // 802.11ax (Wi-Fi 6) channel widths + private const HE_CHANNEL_WIDTHS = [ + 0 => '20/40 MHz', + 1 => '80 MHz', + 2 => '160 MHz' + ]; + + // 802.11be (Wi-Fi 7) channel widths + private const EHT_CHANNEL_WIDTHS = [ + 0 => '20 MHz', + 1 => '40 MHz', + 2 => '80 MHz', + 3 => '160 MHz', + 4 => '320 MHz (6 GHz only)' + ]; public function __construct() { @@ -67,7 +84,23 @@ class HotspotService } /** - * Returns translated security modes. + * Returns 802.11ax (Wi-Fi 6) channel widths + */ + public static function getHeChannelWidths(): array + { + return self::HE_CHANNEL_WIDTHS; + } + + /** + * Returns 802.11be (Wi-Fi 7) channel widths + */ + public static function getEhtChannelWidths(): array + { + return self::EHT_CHANNEL_WIDTHS; + } + + /** + * Returns translated security modes */ public static function getSecurityModes(): array { @@ -94,7 +127,6 @@ class HotspotService ]; } - /** * Validates user input + saves configs for hostapd, dnsmasq & dhcp * @@ -138,7 +170,6 @@ class HotspotService if ($validated === false) { $status->addMessage('Unable to save WiFi hotspot settings due to validation errors', 'danger'); - error_log("HotspotService::validate() -> validated = false"); return false; } @@ -152,6 +183,13 @@ class HotspotService $validated['dualmode'] = !empty($states['DualAPEnable']); $validated['txpower'] = $post_data['txpower']; + // add 802.11ax/be specific parameters if present + if (in_array($validated['hw_mode'], ['ax', 'be'])) { + if ($validated['wpa'] < 4 && $validated['hw_mode'] === 'be') { + $status->addMessage('Note: WiFi 7 works best with WPA3 security', 'info'); + } + } + // hostapd $config = $this->hostapd->buildConfig($validated, $status); $this->hostapd->saveConfig($config, $dualAPEnable, $validated['interface']); diff --git a/src/RaspAP/Networking/Hotspot/Validators/HostapdValidator.php b/src/RaspAP/Networking/Hotspot/Validators/HostapdValidator.php index 83627e11..80dbb832 100644 --- a/src/RaspAP/Networking/Hotspot/Validators/HostapdValidator.php +++ b/src/RaspAP/Networking/Hotspot/Validators/HostapdValidator.php @@ -16,6 +16,15 @@ use RaspAP\Messages\StatusMessage; class HostapdValidator { + // Valid channel widths for 802.11ax (HE) + private const HE_VALID_CHWIDTHS = [0, 1, 2]; // 20/40, 80, 160 MHz + + // Valid channel widths for 802.11be (EHT) + private const EHT_VALID_CHWIDTHS = [0, 1, 2, 3, 4]; // 20, 40, 80, 160, 320 MHz + + // 6 GHz channel range (US) + private const CHANNEL_6GHZ_MIN = 1; + private const CHANNEL_6GHZ_MAX = 233; /** * Validates full hostapd parameter set @@ -64,6 +73,16 @@ class HostapdValidator $goodInput = false; } + // validate 802.11ax specific parameters + if ($post['hw_mode'] === 'ax' && !$this->validateAxParams($post, $status)) { + $goodInput = false; + } + + // validate 802.11be specific parameters + if ($post['hw_mode'] === 'be' && !$this->validateBeParams($post, $status)) { + $goodInput = false; + } + // validate SSID if (empty($post['ssid']) || strlen($post['ssid']) > 32) { $status->addMessage('SSID must be between 1 and 32 characters', 'danger'); @@ -200,9 +219,97 @@ class HostapdValidator 'bridgeStaticIp' => ($post['bridgeStaticIp']), 'bridgeNetmask' => ($post['bridgeNetmask']), 'bridgeGateway' => ($post['bridgeGateway']), - 'bridgeDNS' => ($post['bridgeDNS']) + 'bridgeDNS' => ($post['bridgeDNS']), + 'he_oper_chwidth' => $post['he_oper_chwidth'] ?? null, // 802.11ax parameters + 'he_bss_color' => $post['he_bss_color'] ?? null, // 802.11be parameters + 'eht_oper_chwidth' => $post['eht_oper_chwidth'] ?? null ]; } + /** + * Validates 802.11ax (Wi-Fi 6) specific parameters + * + * @param array $post + * @param StatusMessage $status + * @return bool + */ + private function validateAxParams(array $post, StatusMessage $status): bool + { + $valid = true; + + // Validate HE channel width + if (isset($post['he_oper_chwidth'])) { + $chwidth = (int)$post['he_oper_chwidth']; + if (!in_array($chwidth, self::HE_VALID_CHWIDTHS, true)) { + $status->addMessage('Invalid 802.11ax channel width. Must be 0 (20/40 MHz), 1 (80 MHz), or 2 (160 MHz)', 'danger'); + $valid = false; + } + } + + // Validate BSS color (1-63) + if (isset($post['he_bss_color'])) { + $bssColor = (int)$post['he_bss_color']; + if ($bssColor < 1 || $bssColor > 63) { + $status->addMessage('802.11ax BSS color must be between 1 and 63', 'danger'); + $valid = false; + } + } + + return $valid; + } + + /** + * Validates 802.11be (Wi-Fi 7) specific parameters + * + * @param array $post + * @param StatusMessage $status + * @return bool + */ + private function validateBeParams(array $post, StatusMessage $status): bool + { + $valid = true; + $channel = (int)$post['channel']; + + // Validate EHT channel width + if (isset($post['eht_oper_chwidth'])) { + $chwidth = (int)$post['eht_oper_chwidth']; + + if (!in_array($chwidth, self::EHT_VALID_CHWIDTHS, true)) { + $status->addMessage('Invalid 802.11be channel width. Must be 0-4 (20, 40, 80, 160, or 320 MHz)', 'danger'); + $valid = false; + } + + // 320 MHz only valid on 6 GHz band + if ($chwidth === 4) { + if ($channel < self::CHANNEL_6GHZ_MIN || $channel > self::CHANNEL_6GHZ_MAX) { + $status->addMessage('802.11be 320 MHz channel width is only available on 6 GHz band (channels 1-233)', 'danger'); + $valid = false; + } + } + } + + // Validate BSS color (same as 802.11ax, inherited) + if (isset($post['he_bss_color'])) { + $bssColor = (int)$post['he_bss_color']; + if ($bssColor < 1 || $bssColor > 63) { + $status->addMessage('BSS color must be between 1 and 63', 'danger'); + $valid = false; + } + } + + return $valid; + } + + /** + * Checks if channel is in 6GHz band + * + * @param int $channel + * @return bool + */ + private function is6GHzChannel(int $channel): bool + { + return $channel >= self::CHANNEL_6GHZ_MIN && $channel <= self::CHANNEL_6GHZ_MAX; + } + }