Merge pull request #1992 from RaspAP/feat/80211ax-80211be

Feature: Add support for 802.11ax/be (Wi-Fi 6 / 7)
This commit is contained in:
Bill Zimmerman
2025-12-24 22:39:08 +01:00
committed by GitHub
8 changed files with 599 additions and 29 deletions

View File

@@ -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);

View File

@@ -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'];

View File

@@ -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)

View File

@@ -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",

View File

@@ -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;
}
}
}

View File

@@ -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+';
}
}

View File

@@ -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']);

View File

@@ -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;
}
}