blob: 97a27ce81167bef02d65090568ceaa6ea05ca967 [file] [log] [blame]
#!/usr/bin/python -S
"""Utils for hostapd and wpa_supplicant configuration."""
import subprocess
import Crypto.Protocol.KDF
# pylint: disable=g-bad-import-order
import autochannel
import experiment
import utils
EXPERIMENTS = [
'NoSwapWifiPrimaryChannel', # checked by hostapd itself
'NoAutoNarrowWifiChannel', # checked by hostapd itself
'Wifi80211k',
'WifiBandsteering',
'WifiReverseBandsteering',
'WifiHostapdLogging',
'WifiHostapdDebug',
'WifiShortAggTimeout',
'WifiNoAggTimeout',
]
for _i in EXPERIMENTS:
experiment.register(_i)
# From http://go/alphabet-ie-registry, OUI f4f5e8.
# The properties of this class are hex string representations of varints.
# pylint: disable=invalid-name
class VENDOR_IE_FEATURE_ID(object):
SUPPORTS_PROVISIONING = '01'
PROVISIONING_SSID = '03'
# Recommended HT40/VHT80 settings for given primary channels.
# HT40 channels can fall back to 20 MHz, and VHT80 can fall back to 40 or 20.
# So we configure using a "primary" 20 MHz channel, then allow wider
# transmissions if $width says to do so. These tables are the extra
# information needed to locate the "wider" channels.
# (It might be nicer to use frequencies instead of channels here, but
# unfortunately iw makes conversion back and forth complicated, and the
# math is easier with channel numbers. TODO(apenwarr): Make iw less lame.)
_HT_DIRECTIONS = """
1+ 2+ 3+ 4+ 5- 6- 7+ 8- 9- 10- 11-
36+ 40- 44+ 48-
149+ 153- 157+ 161-
52+ 56- 60+ 64-
100+ 104- 108+ 112-
132+ 136-
"""
_VHT_BASES = """
36=36 40=36 44=36 48=36
149=149 153=149 157=149 161=149
52=52 56=52 60=52 64=52
100=100 104=100 108=100 112=100
"""
_VHT_SETTINGS_TPL = """vht_capab={ampdu}{vht_guard_interval}{ldpc}
vht_oper_chwidth=1
# Wifi channel numbers define the center of a 20 MHz
# channel. 40 MHz channels are defined as one channel plus
# the next one 4 up. 80 MHz channels go back to defining by
# center. If you think about it long enough, you eventually
# discover that the center is 6 channels up from the base 20
# MHz channel, although this isn't very intuitive.
vht_oper_centr_freq_seg0_idx={vht_base}
"""
_HOSTCONF_TPL = """ctrl_interface=/var/run/hostapd
interface={interface}
{bridge}
ssid={ssid}
utf8_ssid=1
auth_algs={auth_algs}
hw_mode={hostapd_band}
channel={channel}
country_code=US
ieee80211d=1
ieee80211h=1
{enable_80211n}
{enable_80211ac}
{enable_wmm}
{require_ht}
{require_vht}
{hidden}
{ap_isolate}
{vendor_elements}
ht_capab={ht20}{ht40}{guard_interval}{ht_rxstbc}
{vht_settings}
"""
_HOSTCONF_WPA_TPL = """wpa={wpa}
wpa_passphrase={psk}
wpa_key_mgmt=WPA-PSK
wpa_pairwise={wpa_pairwise}
"""
_EXPERIMENT_80211K_TPL = """
# 802.11k support
radio_measurements=1
local_pwr_constraint=3
spectrum_mgmt_required=1
neighbor_ap_list_file=/tmp/waveguide/APs.{interface}
"""
_EXTRA_SHORT_TIMEOUTS1_TPL = """
# Enable PTK rekeying (ie. rotate per-station keys, not just group keys).
# People seem to rarely use this but it might improve security.
wpa_ptk_rekey=333
"""
_EXTRA_SHORT_TIMEOUTS2_TPL = """
# Obnoxiously short rekey intervals to maximize the chance of discovering bugs
# caused by rekeying at inopportune times.
wep_rekey_period=10
wpa_group_rekey=10
wpa_strict_rekey=1
wpa_gmk_rekey=9
wpa_ptk_rekey=10
"""
_YOTTASECOND_TIMEOUTS_TPL = """
# Disable all rekeying. This may slightly reduce security but might be
# useful if there are rekeying bugs.
wpa_ptk_rekey=0
wep_rekey_period=0
wpa_group_rekey=0
wpa_strict_rekey=0
wpa_gmk_rekey=0
wpa_ptk_rekey=0
"""
def generate_hostapd_config(
phy_info, interface, band, channel, width, protocols, psk, opt):
"""Generates a hostpad config from the given arguments.
Args:
phy_info: The result of running 'iw phy <phy> info' where <phy> is the phy
on which hostapd will run.
interface: The interface on which hostapd will run.
band: The band on which hostapd will run.
channel: The channel on which hostapd will run.
width: The channel width with which hostapd will run.
protocols: The supported 802.11 protocols, as a collection of
single-character strings (e.g. ['a', 'g', 'n'])
psk: The PSK to use for the AP.
opt: The OptDict parsed from command line options.
Returns:
The generated hostapd config, as a string.
Raises:
ValueError: For certain invalid combinations of arguments.
"""
utils.log('generating configuration...')
if band == '2.4':
hostapd_band = 'g' if set(('n', 'g')) & protocols else 'b'
else:
hostapd_band = 'a'
ampdu = ''
enable_80211n = ''
enable_80211ac = ''
require_ht = ''
require_vht = ''
ht20 = ''
ht40 = ''
ht_rxstbc = ''
vht_settings = ''
guard_interval = (
'[SHORT-GI-20][SHORT-GI-40]' if opt.short_guard_interval else '')
vht_guard_interval = '[SHORT-GI-80]' if opt.short_guard_interval else ''
if 'RX STBC 3-stream' in phy_info:
ht_rxstbc = '[RX-STBC123]'
if 'RX STBC 2-stream' in phy_info:
ht_rxstbc = '[RX-STBC12]'
if 'RX STBC 1-stream' in phy_info:
ht_rxstbc = '[RX-STBC1]'
if 'n' in protocols:
enable_80211n = 'ieee80211n=1'
ht20 = '[HT20]'
if 'ac' in protocols:
if width == '80':
enable_80211ac = 'ieee80211ac=1'
if 'Maximum RX AMPDU length 16383 bytes' in phy_info:
ampdu = '[MAX-A-MPDU-LEN-EXP1]'
if 'Maximum RX AMPDU length 32767 bytes' in phy_info:
ampdu = '[MAX-A-MPDU-LEN-EXP2]'
if 'Maximum RX AMPDU length 65535 bytes' in phy_info:
ampdu = '[MAX-A-MPDU-LEN-EXP3]'
if 'Maximum RX AMPDU length 131071 bytes' in phy_info:
ampdu = '[MAX-A-MPDU-LEN-EXP4]'
if 'Maximum RX AMPDU length 262143 bytes' in phy_info:
ampdu = '[MAX-A-MPDU-LEN-EXP5]'
if 'Maximum RX AMPDU length 524287 bytes' in phy_info:
ampdu = '[MAX-A-MPDU-LEN-EXP6]'
if 'Maximum RX AMPDU length 1048575 bytes' in phy_info:
ampdu = '[MAX-A-MPDU-LEN-EXP7]'
if not set(('a', 'b', 'ab', 'g')) & protocols:
require_ht = 'require_ht=1'
if not set(('a', 'b', 'ab', 'g', 'n')) & protocols:
require_vht = 'require_vht=1'
if opt.encryption.startswith('WPA_PSK_'):
auth_algs, wpa = 1, 1
elif opt.encryption.startswith('WPA2_PSK_'):
auth_algs, wpa = 1, 2
elif opt.encryption.startswith('WPA12_PSK_'):
auth_algs, wpa = 1, 3
elif opt.encryption.startswith('WEP'):
auth_algs, wpa = 3, 0
elif opt.encryption.startswith('NONE'):
auth_algs, wpa = 1, 0
else:
raise ValueError('Invalid crypto protocol: %s' % opt.encryption)
if opt.encryption[-4:] in ('_AES', 'WEP', 'NONE'):
wpa_pairwise = 'CCMP'
elif opt.encryption.endswith('_TKIP'):
wpa_pairwise = 'TKIP'
else:
raise ValueError('Invalid crypto protocol: %s' % opt.encryption)
if int(width) >= 40:
if '%s+' % channel in _HT_DIRECTIONS.split():
ht40 = '[HT40+]'
elif '%s-' % channel in _HT_DIRECTIONS.split():
ht40 = '[HT40-]'
else:
raise ValueError(
'HT40 requested but not available on channel %s.' % channel)
if width == '80':
ldpc = '[RXLDPC]'
vht_base = ''
for base in _VHT_BASES.split():
if base.startswith('%s=' % channel):
vht_base = base.split('=')[1]
break
if vht_base:
vht_settings = _VHT_SETTINGS_TPL.format(
ampdu=ampdu,
vht_guard_interval=vht_guard_interval,
ldpc=ldpc, vht_base=int(vht_base) + 6)
else:
raise ValueError(
'VHT80 requested but not available on channel %s' % channel)
try:
bssid = None
if subprocess.call(('is-network-box')) == 0:
mac_addr_hnvram = (
'MAC_ADDR_WIFI' + ('' if interface.startswith('wlan0') else '2'))
bssid = utils.subprocess_output_or_none(
('hnvram', '-qr', mac_addr_hnvram))
if bssid is None:
bssid = utils.subprocess_output_or_none(('hnvram', '-rq', 'MAC_ADDR'))
if bssid is None:
raise utils.BinWifiException(
'Box has no MAC_ADDR_WIFI, MAC_ADDR_WIFI2, or MAC_ADDR. You can '
'set these with e.g. '
"'# hnvram -w MAC_ADDR_WIFI=00:00:00:00:00:00'")
except OSError:
pass
enable_wmm = 'wmm_enabled=1' if opt.enable_wmm else ''
hidden = 'ignore_broadcast_ssid=1' if opt.hidden_mode else ''
bridge = 'bridge=%s' % opt.bridge if opt.bridge else ''
ap_isolate = 'ap_isolate=1' if opt.client_isolation else ''
hostapd_conf_parts = [_HOSTCONF_TPL.format(
interface=interface, band=band, channel=channel, width=width,
protocols=protocols, hostapd_band=hostapd_band,
enable_80211n=enable_80211n, enable_80211ac=enable_80211ac,
require_ht=require_ht, require_vht=require_vht, ht20=ht20, ht40=ht40,
ht_rxstbc=ht_rxstbc, vht_settings=vht_settings,
guard_interval=guard_interval, enable_wmm=enable_wmm, hidden=hidden,
ap_isolate=ap_isolate, auth_algs=auth_algs, bridge=bridge,
ssid=utils.sanitize_ssid(opt.ssid),
vendor_elements=get_vendor_elements(opt))]
if opt.encryption != 'NONE':
hostapd_conf_parts.append(_HOSTCONF_WPA_TPL.format(
psk=utils.validate_and_sanitize_psk(psk), wpa=wpa,
wpa_pairwise=wpa_pairwise))
if experiment.enabled('Wifi80211k'):
hostapd_conf_parts.append(
_EXPERIMENT_80211K_TPL.format(interface=interface))
if opt.yottasecond_timeouts:
hostapd_conf_parts.append(_YOTTASECOND_TIMEOUTS_TPL)
elif opt.extra_short_timeouts >= 2:
hostapd_conf_parts.append(_EXTRA_SHORT_TIMEOUTS2_TPL)
elif opt.extra_short_timeouts >= 1:
hostapd_conf_parts.append(_EXTRA_SHORT_TIMEOUTS1_TPL)
# Track the active experiments the last time hostapd was started:
# - for easier examination of the state
# - to make sure the config counts as changed whenever the set of
# experiments changes.
active_experiments = [i for i in EXPERIMENTS
if experiment.enabled(i)]
hostapd_conf_parts.append('# Experiments: (%s)\n'
% ','.join(active_experiments))
utils.log('configuration ready.')
return '\n'.join(hostapd_conf_parts)
def make_network_block(network_block_lines):
return 'network={\n%s\n}\n' % '\n'.join(network_block_lines)
def open_network_lines(ssid):
return ['\tssid="%s"' % utils.sanitize_ssid(ssid),
'\tkey_mgmt=NONE']
def wpa_network_lines(ssid, passphrase):
"""Like `wpa_passphrase "$ssid" "$passphrase"`, but more convenient output.
This generates raw config lines, so we can update the config when the defaults
don't make sense for us without doing parsing.
N.b. wpa_passphrase double quotes provided SSID and passphrase arguments, and
does not escape quotes or backslashes.
Args:
ssid: a wifi network SSID
passphrase: a wifi network PSK
Returns:
lines of a network block that will let wpa_supplicant join this network
"""
clean_ssid = utils.sanitize_ssid(ssid)
network_lines = ['\tssid="%s"' % clean_ssid]
clean_passphrase = utils.validate_and_sanitize_psk(passphrase)
if len(clean_passphrase) == 64:
network_lines += ['\tpsk=%s' % clean_passphrase]
else:
raw_psk = Crypto.Protocol.KDF.PBKDF2(clean_passphrase, clean_ssid, 32, 4096)
hex_psk = ''.join(ch.encode('hex') for ch in raw_psk)
network_lines += ['\t#psk="%s"' % clean_passphrase, '\tpsk=%s' % hex_psk]
return network_lines
def generate_wpa_supplicant_config(ssid, passphrase, opt):
"""Generate a wpa_supplicant config from the provided arguments."""
if passphrase is None:
network_block_lines = open_network_lines(ssid)
else:
network_block_lines = wpa_network_lines(ssid, passphrase)
network_block_lines.append('\tscan_ssid=1')
if opt.bssid:
network_block_lines.append('\tbssid=%s' %
utils.validate_and_sanitize_bssid(opt.bssid))
network_block = make_network_block(network_block_lines)
freq_list = ' '.join(autochannel.get_all_frequencies(opt.band))
lines = [
'ctrl_interface=/var/run/wpa_supplicant',
'ap_scan=1',
'autoscan=exponential:1:30',
'freq_list=' + freq_list,
network_block
]
return '\n'.join(lines)
def create_vendor_ie(feature_id, payload=''):
"""Create a vendor IE in hostapd config format.
Args:
feature_id: The go/alphabet-ie-registry feature ID for OUI f4f5e8.
payload: A string payload (must be ASCII), or none.
Returns:
The vendor IE, as a string.
"""
length = '%02x' % (3 + (len(feature_id)/2) + len(payload))
oui = 'f4f5e8'
return 'dd%s%s%s%s' % (length, oui, feature_id, payload.encode('hex'))
def get_vendor_elements(opt):
"""Get vendor_elements value hostapd config.
The way to specify multiple vendor IEs in hostapd is to concatenate them, e.g.
vendor_elements=dd0411223301dd051122330203
Args:
opt: The optdict containing user-specified options.
Returns:
The vendor_elements string (including that prefix, or empty if there are no
vendor IEs.)
"""
vendor_ies = []
if opt.supports_provisioning:
vendor_ies.append(
create_vendor_ie(VENDOR_IE_FEATURE_ID.SUPPORTS_PROVISIONING))
if opt.hidden_mode:
vendor_ies.append(
create_vendor_ie(VENDOR_IE_FEATURE_ID.PROVISIONING_SSID, opt.ssid))
if vendor_ies:
return 'vendor_elements=%s' % ''.join(vendor_ies)
return ''