blob: 8797633967761a89d6353684c7cf7f45d91e4511 [file] [log] [blame]
#!/usr/bin/python
"""Utility for controlling WiFi AP and client functionality."""
from __future__ import print_function
import atexit
import glob
import os
import re
import subprocess
import sys
import time
import autochannel
import bandsteering
import configs
import experiment
import iw
import options
import persist
import quantenna
import utils
_OPTSPEC_FORMAT = """
{bin} set Enable or modify access points. Takes all options unless otherwise specified.
{bin} setclient Enable or modify wifi clients. Takes -b, -P, -s, --bssid, -S.
{bin} stop|off Disable access points and clients. Takes -b, -P, -S.
{bin} stopap Disable access points. Takes -b, -P, -S.
{bin} stopclient Disable wifi clients. Takes -b, -P, -S.
{bin} restore Restore saved client and access point options. Takes -b, -S.
{bin} show Print all known parameters. Takes -b, -S.
{bin} scan Print 'iw scan' results for a single band. Takes -b, -S.
--
b,band= Wifi band(s) to use (5 GHz and/or 2.4 GHz). set, setclient, and scan have a default of 2.4 and cannot take multiple-band values. [2.4 5]
c,channel= Channel to use [auto]
a,autotype= Autochannel method to use (LOW, HIGH, DFS, NONDFS, ANY,OVERLAP) [NONDFS]
s,ssid= SSID to use [{ssid}]
bssid= BSSID to use []
e,encryption= Encryption type to use (WPA_PSK_AES, WPA2_PSK_AES, WPA12_PSK_AES, WPA_PSK_TKIP, WPA2_PSK_TKIP, WPA12_PSK_TKIP, WEP, or NONE) [WPA2_PSK_AES]
f,force-restart Force restart even if already running with these options
C,client-isolation Enable client isolation, preventing bridging of frames between associated stations.
H,hidden-mode Enable hidden mode (disable SSID advertisements)
M,enable-wmm Enable wmm extensions (needed for block acks)
G,short-guard-interval Enable short guard interval
p,protocols= 802.11 levels to allow, slash-delimited [a/b/g/n/ac]
w,width= Channel width to use, in MHz (20, 40, or 80) [20]
B,bridge= Bridge device to use [br0]
X,extra-short-timeouts Use shorter key rotations; 1=rotate PTK, 2=rotate often
Y,yottasecond-timeouts Don't rotate any keys: PTK, GTK, or GMK
P,persist For set commands, persist options so we can restore them with 'wifi restore'. For stop commands, remove persisted options.
S,interface-suffix= Interface suffix []
lock-timeout= How long, in seconds, to wait for another /bin/wifi process to finish before giving up. [60]
scan-ap-force (Scan only) scan when in AP mode
scan-passive (Scan only) do not probe, scan passively
scan-freq= (Scan only) limit scan to specific frequencies.
supports-provisioning Indicate via vendor IE that this AP supports provisioning. Corresponds to feature ID 01 of OUI f4f5e8 at go/alphabet-ie-registry.
"""
_FINGERPRINTS_DIRECTORY = '/tmp/wifi/fingerprints'
_LOCKFILE = '/tmp/wifi/wifi'
lockfile_taken = False
# pylint: disable=protected-access
subprocess.call(('mkdir', '-p', utils._CONFIG_DIR))
def _stop_hostapd(interface):
"""Stops hostapd from running on the given interface.
Also removes the pid file, sets them interface down and deletes the monitor
interface, if it exists.
Args:
interface: The interface on which to stop hostapd.
Returns:
Whether hostapd was successfully stopped and cleaned up.
"""
if not _is_hostapd_running(interface):
utils.log('hostapd already not running.')
return True
config_filename = utils.get_filename(
'hostapd', utils.FILENAME_KIND.config, interface, tmp=True)
pid_filename = utils.get_filename(
'hostapd', utils.FILENAME_KIND.pid, interface, tmp=True)
if not utils.kill_pid('hostapd .* %s$' % config_filename, pid_filename):
return False
# TODO(apenwarr): hostapd doesn't always delete interface mon.$ifc. Then it
# gets confused by the fact that it already exists. Let's help out. We
# should really fix this by eliminating the need for hostapd to have a
# monitor interface at all (which is deprecated anyway) Remove this line when
# our hostapd no longer needs a monitor interface.
utils.subprocess_quiet(('iw', 'dev', 'mon.%s' % interface, 'del'))
subprocess.check_call(('ip', 'link', 'set', interface, 'down'))
return True
def _stop_wpa_supplicant(interface):
"""Stops wpa_supplicant from running on the given interface.
Also removes the pid file and sets the interface down.
Args:
interface: The interface on which to stop wpa_supplicant.
Returns:
Whether wpa_supplicant was successfully stopped.
"""
if not _is_wpa_supplicant_running(interface):
utils.log('wpa_supplicant already not running.')
return True
pid_filename = utils.get_filename(
'wpa_supplicant', utils.FILENAME_KIND.pid, interface, tmp=True)
config_filename = utils.get_filename(
'wpa_supplicant', utils.FILENAME_KIND.config, interface, tmp=True)
if not utils.kill_pid('wpa_supplicant .* %s$' % config_filename,
pid_filename):
return False
try:
subprocess.check_call(('ip', 'link', 'set', interface, 'down'))
except subprocess.CalledProcessError:
return False
return True
def _set_wifi_broadcom(opt):
"""Set up wifi using wl, for Broadcom chips.
Args:
opt: The OptDict parsed from command line options.
Raises:
BinWifiException: On various errors.
"""
def wl(*args):
utils.log('wl %s', ' '.join(args))
subprocess.check_call(('wl') + list(args))
utils.log('Configuring broadcom wifi.')
wl('radio', 'on')
wl('down')
wl('ssid', '')
band = opt.band
if opt.channel != 'auto':
band = 'auto'
try:
wl('band', {'2.4': 'b', '5': 'a', 'auto': 'auto'}[band])
except KeyError:
raise utils.BinWifiException('Invalid band %s', band)
wl('ap', '0')
wl('up')
if opt.channel == 'auto':
# We can only run autochannel when ap=0, but setting ap=1 later will wipe
# the value. So we have to capture the autochannel setting, then set it
# later. 'wl autochannel 2' is thus useless.
wl('autochannel', '1')
# enough time to scan all the 2.4 or 5 GHz channels at 100ms each
time.sleep(3)
utils.log('wl autochannel')
channel = subprocess.check_output(('wl', 'autochannel')).split()[0]
wl('ap', '1')
wl('chanspec', channel)
wl('auth', '0')
wl('infra', '1')
try:
wl('wsec', {'_AES': '4',
'TKIP': '2',
'WEP': '1',
'NONE': '0'}[opt.encryption[-4:]])
except KeyError:
raise utils.BinWifiException('invalid crypto %s', opt.encryption)
wl('sup_wpa', '1')
try:
wl('wpa_auth',
{'WPA_': '4',
'WPA2': '128',
'WEP': '0',
'NONE': '0'}[opt.encryption[:4]])
except KeyError:
raise utils.BinWifiException('invalid crypto %s', opt.encryption)
wl('up')
if '_PSK_' in opt.encryption:
# WPA keys must be added *before* setting the SSID
wl('set_pmk', os.environ['WIFI_PSK'])
wl('ssid', opt.ssid)
elif opt.encryption == 'WEP':
# WEP keys must be added *after* setting the SSID
wl('ssid', opt.ssid)
wl('set_pmk', os.environ['WIFI_PSK'])
elif opt.encryption == 'NONE':
wl('ssid', opt.ssid)
else:
raise utils.BinWifiException('invalid crypto %s', opt.encryption)
def set_wifi(opt):
"""Set up an access point in response to the 'set' command.
Args:
opt: The OptDict parsed from command line options.
Returns:
Whether setting up the AP succeeded.
Raises:
BinWifiException: On various errors.
"""
band = opt.band
width = opt.width
channel = opt.channel
autotype = opt.autotype
protocols = set(opt.protocols.split('/'))
utils.validate_set_wifi_options(
band, width, autotype, protocols, opt.encryption)
psk = None
if opt.encryption == 'WEP' or '_PSK_' in opt.encryption:
psk = os.environ['WIFI_PSK']
if band == '5' and quantenna.set_wifi(opt):
return True
if iw.RUNNABLE_WL() and not iw.RUNNABLE_IW():
_set_wifi_broadcom(opt)
return True
if not iw.RUNNABLE_IW():
raise utils.BinWifiException("Can't proceed without iw")
# If this phy is running client mode, we need to use its width/channel.
phy = iw.find_phy(band, channel)
if phy is None:
raise utils.BinWifiException(
'no wifi phy for band=%s channel=%s', band, channel)
interface = iw.find_interface_from_phy(
phy, iw.INTERFACE_TYPE.ap, opt.interface_suffix)
if interface is None:
raise utils.BinWifiException(
'no wifi interface found for band=%s channel=%s suffix=%s',
band, channel, opt.interface_suffix)
found_active_config = False
for other_interface in (set(iw.find_all_interfaces_from_phy(phy)) -
set([interface])):
if _is_wpa_supplicant_running(other_interface):
get_band = _get_wpa_band
elif _is_hostapd_running(other_interface):
get_band = _get_hostapd_band
else:
continue
# Wait up to ten seconds for interface width and channel to be available
# (only relevant if hostapd/wpa_supplicant was started recently).
# TODO(rofrankel): Consider shortcutting this loop if wpa_cli shows status
# is SCANNING (and other values)?
utils.log('Interface %s running on same band; finding its width and '
'channel.', other_interface)
for _ in xrange(50):
active_band = get_band(other_interface)
active_width, active_channel = iw.find_width_and_channel(other_interface)
sys.stderr.write('.')
if None not in (active_band, active_width, active_channel):
band, width, channel = active_band, active_width, active_channel
utils.log('Using band=%s, channel=%s, width=%s MHz from interface %s',
band, channel, width, other_interface)
found_active_config = True
break
time.sleep(0.2)
else:
utils.log("Couldn't find band, width, and channel used by interface %s "
"(it may not be connected)", other_interface)
if found_active_config:
break
utils.log('interface: %s', interface)
utils.log('Configuring cfg80211 wifi.')
pid_filename = utils.get_filename(
'hostapd', utils.FILENAME_KIND.pid, interface, tmp=True)
utils.log('pidfile: %s', pid_filename)
autotype_filename = '/tmp/autotype.%s' % interface
band_filename = '/tmp/band.%s' % interface
width_filename = '/tmp/width.%s' % interface
autochan_filename = '/tmp/autochan.%s' % interface
old_autotype = utils.read_or_empty(autotype_filename)
old_band = utils.read_or_empty(band_filename)
old_width = utils.read_or_empty(width_filename)
# Special case: if autochannel enabled and we've done it before, just use the
# old autochannel. The main reason for this is we may not be able to run the
# autochannel algorithm without stopping hostapd first, which defeats the code
# that tries not to restart hostapd unnecessarily.
if (channel == 'auto'
and (autotype, band, width) == (old_autotype, old_band, old_width)):
# ...but only if not forced mode. If it's forced, don't use the old
# value, but don't wipe it either.
if not opt.force_restart:
autochan = utils.read_or_empty(autochan_filename)
if autochan and int(autochan) > 0:
utils.log('Reusing old autochannel=%s', autochan)
channel = autochan
else:
# forget old autochannel setting
if os.path.exists(autochan_filename):
try:
os.remove(autochan_filename)
except OSError:
utils.log('Failed to remove autochan file.')
if channel == 'auto':
utils.atomic_write(autochan_filename, '')
try:
channel = autochannel.scan(interface, band, autotype, width)
except ValueError as e:
raise utils.BinWifiException('Autochannel scan failed: %s', e)
utils.atomic_write(autochan_filename, channel)
utils.atomic_write(autotype_filename, autotype)
utils.atomic_write(band_filename, band)
utils.atomic_write(width_filename, width)
utils.log('using channel=%s', channel)
try:
utils.log('getting phy info...')
with open(os.devnull, 'w') as devnull:
try:
phy_info = subprocess.check_output(('iw', 'phy', phy, 'info'),
stderr=devnull)
except subprocess.CalledProcessError as e:
raise utils.BinWifiException(
'Failed to get phy info for phy %s: %s', phy, e)
hostapd_config = configs.generate_hostapd_config(
phy_info, interface, band, channel, width, protocols, psk, opt)
except ValueError as e:
raise utils.BinWifiException('Invalid option: %s', e)
return _maybe_restart_hostapd(interface, hostapd_config, opt)
@iw.requires_iw
def stop_wifi(opt):
return stop_client_wifi(opt) and stop_ap_wifi(opt)
def stop_ap_wifi(opt):
"""Disable an access point in response to the 'setap' command.
Args:
opt: The OptDict parsed from command line options.
Returns:
Whether disabling the AP succeeded.
Raises:
BinWifiException: If an expected interface is not found.
"""
success = True
for band in opt.band.split():
utils.log('stopping AP for %s GHz...', band)
if band == '5' and quantenna.stop_ap_wifi(opt):
continue
interface = iw.find_interface_from_band(
band, iw.INTERFACE_TYPE.ap, opt.interface_suffix)
if interface is None:
utils.log('No AP interface for %s GHz; nothing to stop', band)
continue
if _stop_hostapd(interface):
if opt.persist:
persist.delete_options('hostapd', band)
else:
utils.log('Failed to stop hostapd on interface %s', interface)
success = False
return success
@iw.requires_iw
def _restore_wifi(band, program):
"""Restore a program from persisted settings.
Args:
band: The band on which to restore program.
program: The program to restore (wpa_supplicant or hostapd).
Returns:
Whether whether restoring succeeded, but may die.
"""
argv = persist.load_options(program, band, False)
if argv is None:
utils.log('No persisted options for %s GHz %s, not restoring',
band, program)
return False
utils.log('Loaded persisted options for %s GHz %s', band, program)
if _run(argv):
utils.log('Restored %s for %s GHz', program, band)
return True
utils.log('Failed to restore %s for %s GHz', program, band)
return False
def restore_wifi(opt):
"""Restore hostapd and wpa_supplicant on both bands from persisted settings.
Nothing happens if persisted settings are not available.
Args:
opt: The OptDict parsed from command line options.
Returns:
True.
"""
# If both bands are specified, restore 5 GHz first so that STAs are more
# likely to join it.
for band in sorted(opt.band.split(),
reverse=not experiment.enabled('WifiReverseBandsteering')):
_restore_wifi(band, 'wpa_supplicant')
_restore_wifi(band, 'hostapd')
return True
# TODO(apenwarr): Extend this to notice actual running settings.
# Including whether hostapd is up or down, etc.
# TODO(rofrankel): Extend this to show client interface info.
@iw.requires_iw
def show_wifi(opt):
"""Prints information about wifi interfaces on this device.
Args:
opt: The OptDict parsed from command line options.
Returns:
True.
"""
for band in opt.band.split():
print('Band: %s' % band)
for tokens in utils.subprocess_line_tokens(('iw', 'reg', 'get')):
if len(tokens) >= 2 and tokens[0] == 'country':
print('RegDomain: %s' % tokens[1].strip(':'))
break
for prefix, interface_type in (('', iw.INTERFACE_TYPE.ap),
('Client ', iw.INTERFACE_TYPE.client)):
interface = iw.find_interface_from_band(
band, interface_type, opt.interface_suffix)
if interface is None:
continue
print('%sInterface: %s # %s GHz %s' %
(prefix, interface, band, 'client' if 'cli' in interface else 'ap'))
info_parsed = iw.info_parsed(interface)
for k, label in (('channel', 'Channel'),
('ssid', 'SSID'),
('addr', 'BSSID')):
v = info_parsed.get(k, None)
if v is not None:
print('%s%s: %s' % (prefix, label, v))
if interface_type == iw.INTERFACE_TYPE.ap:
print('AutoChannel: %r' %
os.path.exists('/tmp/autochan.%s' % interface))
try:
with open('/tmp/autotype.%s' % interface) as autotype:
print('AutoType: %s' % autotype.read().strip())
except IOError:
pass
print('Station List for band: %s' % band)
station_dump = iw.station_dump(interface)
if station_dump:
print(station_dump)
print()
return True
@iw.requires_iw
def scan_wifi(opt):
"""Prints 'iw scan' results.
Args:
opt: The OptDict parsed from command line options.
Returns:
True.
Raises:
BinWifiException: If an expected interface is not found.
"""
band = opt.band.split()[0]
if band == '5' and quantenna.scan_wifi(opt):
return True
interface = iw.find_interface_from_band(
band, iw.INTERFACE_TYPE.ap, opt.interface_suffix)
if interface is None:
raise utils.BinWifiException('No client interface for band %s', band)
scan_args = []
if opt.scan_ap_force:
scan_args += ['ap-force']
if opt.scan_passive:
scan_args += ['passive']
if opt.scan_freq:
scan_args += ['freq', opt.scan_freq]
print(iw.scan(interface, scan_args))
return True
def _is_hostapd_running(interface):
return utils.subprocess_quiet(
('hostapd_cli', '-i', interface, 'status'), no_stdout=True) == 0
def _wpa_cli(program, interface, command):
return utils.subprocess_quiet(
(program, '-i', interface, command), no_stdout=True) == 0
def _is_wpa_supplicant_running(interface):
return _wpa_cli('wpa_cli', interface, 'status')
def _reconfigure_wpa_supplicant(interface):
if not _wpa_cli('wpa_cli', interface, 'reconfigure'):
return False
return _wait_for_wpa_supplicant_to_associate(interface)
def _hostapd_debug_options():
if experiment.enabled('WifiHostapdDebug'):
return ['-d']
else:
return []
def _get_hostapd_band(interface):
for line in utils.subprocess_lines(
('hostapd_cli', '-i', interface, 'status')):
tokens = line.split('=')
if tokens and tokens[0] == 'freq':
try:
return {'5': '5', '2': '2.4'}[tokens[1][0]]
except (IndexError, KeyError):
return None
def _start_hostapd(interface, config_filename, band, ssid):
"""Starts a babysat hostapd.
Args:
interface: The interface on which to start hostapd.
config_filename: The filename of the hostapd configuration.
band: The band on which hostapd is being started.
ssid: The SSID with which hostapd is being started.
Returns:
Whether hostapd was started successfully.
"""
aggfiles = glob.glob('/sys/kernel/debug/ieee80211/phy*/' +
'netdev:%s/default_agg_timeout' % interface)
if not aggfiles:
# This can happen on non-mac80211 interfaces.
utils.log('agg_timeout: no default_agg_timeout files for %r', interface)
else:
if experiment.enabled('WifiShortAggTimeout'):
utils.log('Using short agg_timeout.')
agg = 500
elif experiment.enabled('WifiNoAggTimeout'):
utils.log('Disabling agg_timeout.')
agg = 0
else:
utils.log('Using default long agg_timeout.')
agg = 5000
for aggfile in aggfiles:
open(aggfile, 'w').write(str(agg))
pid_filename = utils.get_filename(
'hostapd', utils.FILENAME_KIND.pid, interface, tmp=True)
alivemonitor_filename = utils.get_filename(
'hostapd', utils.FILENAME_KIND.alive, interface, tmp=True)
utils.log('Starting hostapd.')
utils.babysit(['alivemonitor', alivemonitor_filename, '30', '2', '65',
'hostapd',
'-A', alivemonitor_filename,
'-F', _FINGERPRINTS_DIRECTORY] +
bandsteering.hostapd_options(band, ssid) +
_hostapd_debug_options() +
[config_filename],
'hostapd-%s' % interface, 10, pid_filename)
# Wait for hostapd to start, and return False if it doesn't.
for _ in xrange(40):
if utils.check_pid(pid_filename):
break
sys.stderr.write('.')
time.sleep(0.1)
else:
return False
# hostapd_cli returns success on command timeouts. If we time this
# perfectly and manage to connect but then wpa_supplicant dies right after,
# we'd think it succeeded. So sleep a bit to try to give wpa_supplicant a
# chance to die from its error before we try to connect to it.
time.sleep(0.5)
for _ in xrange(10):
if not utils.check_pid(pid_filename):
break
if _is_hostapd_running(interface):
utils.log('ok')
return True
sys.stderr.write('.')
time.sleep(0.1)
return False
def _get_wpa_state(interface):
for line in utils.subprocess_lines(('wpa_cli', '-i', interface, 'status')):
tokens = line.split('=')
if tokens and tokens[0] == 'wpa_state':
return tokens[1]
def _get_wpa_band(interface):
for line in utils.subprocess_lines(('wpa_cli', '-i', interface, 'status')):
tokens = line.split('=')
if tokens and tokens[0] == 'freq':
try:
return {'5': '5', '2': '2.4'}[tokens[1][0]]
except (IndexError, KeyError):
return None
def _wait_for_wpa_supplicant_to_associate(interface):
"""Wait for wpa_supplicant to associate.
If it does not associate within a certain period of time, terminate it.
Args:
interface: The interface on which wpa_supplicant is running.
Raises:
BinWifiException: if wpa_supplicant fails to associate and
also cannot be stopped to cleanup after the failure.
Returns:
Whether wpa_supplicant associated within the timeout.
"""
utils.log('Waiting for wpa_supplicant to connect')
for _ in xrange(100):
if _get_wpa_state(interface) == 'COMPLETED':
utils.log('ok')
return True
sys.stderr.write('.')
time.sleep(0.1)
utils.log('wpa_supplicant did not connect.')
if not _stop_wpa_supplicant(interface):
raise utils.BinWifiException(
"Couldn't stop wpa_supplicant after it failed to connect. "
"Consider killing it manually.")
return False
def _start_wpa_supplicant(interface, config_filename):
"""Starts a babysat wpa_supplicant.
Args:
interface: The interface on which to start wpa_supplicant.
config_filename: The filename of the wpa_supplicant configuration.
Raises:
BinWifiException: if wpa_supplicant fails to connect and
also cannot be stopped to cleanup after the failure.
Returns:
Whether wpa_supplicant was started successfully.
"""
pid_filename = utils.get_filename(
'wpa_supplicant', utils.FILENAME_KIND.pid, interface, tmp=True)
utils.log('Starting wpa_supplicant.')
utils.babysit(['wpa_supplicant',
'-Dnl80211',
'-i', interface,
'-c', config_filename] +
_hostapd_debug_options(),
'wpa_supplicant-%s' % interface, 10, pid_filename)
# Wait for wpa_supplicant to start, and return False if it doesn't.
for _ in xrange(10):
if utils.check_pid(pid_filename):
break
sys.stderr.write('.')
time.sleep(0.1)
else:
return False
# wpa_supplicant_cli returns success on command timeouts. If we time this
# perfectly and manage to connect but then wpa_supplicant dies right after,
# we'd think it succeeded. So sleep a bit to try to give wpa_supplicant a
# chance to die from its error before we try to connect to it.
time.sleep(0.5)
for _ in xrange(50):
if not utils.check_pid(pid_filename):
utils.log('wpa_supplicant process died.')
return False
if _is_wpa_supplicant_running(interface):
break
sys.stderr.write('.')
time.sleep(0.1)
else:
return False
return _wait_for_wpa_supplicant_to_associate(interface)
def _maybe_restart_hostapd(interface, config, opt):
"""Starts or restarts hostapd unless doing so would be a no-op.
The no-op case (i.e. hostapd is already running with an equivalent config) can
be overridden with --force-restart.
Args:
interface: The interface on which to start hostapd.
config: A hostapd configuration, as a string.
opt: The OptDict parsed from command line options.
Returns:
Whether hostapd was started successfully.
Raises:
BinWifiException: On various errors.
"""
tmp_config_filename = utils.get_filename(
'hostapd', utils.FILENAME_KIND.config, interface, tmp=True)
forced = False
current_config = None
try:
with open(tmp_config_filename) as tmp_config_file:
current_config = tmp_config_file.read()
except IOError:
pass
if not _is_hostapd_running(interface):
utils.log('hostapd not running yet, starting.')
elif current_config != config:
utils.log('hostapd config changed, restarting.')
elif opt.force_restart:
utils.log('Forced restart requested.')
forced = True
else:
utils.log('hostapd-%s already configured and running', interface)
return True
if not _stop_hostapd(interface):
raise utils.BinWifiException("Couldn't stop hostapd")
# We don't want to try to rewrite this file if this is just a forced restart.
if not forced:
utils.atomic_write(tmp_config_filename, config)
if not _start_hostapd(interface, tmp_config_filename, opt.band, opt.ssid):
utils.log('hostapd failed to start. Look at hostapd logs for details.')
return False
return True
def _restart_hostapd(band):
"""Restart hostapd from previous options.
Only used by _set_wpa_supplicant_config, to restart hostapd after stopping it.
Args:
band: The band on which to restart hostapd.
Returns:
Whether hostapd was successfully restarted.
Raises:
BinWifiException: If reading previous settings fails.
"""
argv = persist.load_options('hostapd', band, True)
if argv is None:
raise utils.BinWifiException('Failed to read previous hostapd config')
_run(argv)
def _set_wpa_supplicant_config(interface, config, opt):
"""Starts or restarts wpa_supplicant unless doing so would be a no-op.
The no-op case (i.e. wpa_supplicant is already running with an equivalent
config) can be overridden with --force-restart.
Args:
interface: The interface on which to start wpa_supplicant.
config: A wpa_supplicant configuration, as a string.
opt: The OptDict parsed from command line options.
Returns:
Whether wpa_supplicant was started successfully.
Raises:
BinWifiException: On various errors.
"""
tmp_config_filename = utils.get_filename(
'wpa_supplicant', utils.FILENAME_KIND.config, interface, tmp=True)
forced = False
current_config = None
band = opt.band
try:
with open(tmp_config_filename) as tmp_config_file:
current_config = tmp_config_file.read()
except IOError:
pass
already_running = _is_wpa_supplicant_running(interface)
if not already_running:
utils.log('wpa_supplicant not running yet, starting.')
elif current_config != config:
# TODO(rofrankel): Consider using wpa_cli reconfigure here.
utils.log('wpa_supplicant config changed, reconfiguring.')
elif opt.force_restart:
utils.log('Forced restart requested.')
forced = True
else:
utils.log('wpa_supplicant-%s already configured and running', interface)
return True
if not forced:
utils.atomic_write(tmp_config_filename, config)
# TODO(rofrankel): Consider removing all the restart hostapd stuff when
# b/30140131 is resolved. hostapd seems to keep working without being
# restarted, at least on Camaro.
restart_hostapd = False
ap_interface = iw.find_interface_from_band(band, iw.INTERFACE_TYPE.ap,
opt.interface_suffix)
if _is_hostapd_running(ap_interface):
restart_hostapd = True
opt_without_persist = options.OptDict({})
opt_without_persist.persist = False
opt_without_persist.band = opt.band
opt_without_persist.interface_suffix = opt.interface_suffix
if not stop_ap_wifi(opt_without_persist):
raise utils.BinWifiException(
"Couldn't stop hostapd to start wpa_supplicant.")
if already_running:
if not _reconfigure_wpa_supplicant(interface):
raise utils.BinWifiException('Failed to reconfigure wpa_supplicant.')
elif not _start_wpa_supplicant(interface, tmp_config_filename):
raise utils.BinWifiException(
'wpa_supplicant failed to start. Look at wpa_supplicant logs for '
'details.')
if restart_hostapd:
_restart_hostapd(band)
return True
@iw.requires_iw
def client_interface_name(phy, interface_suffix):
ap_interface = iw.find_interface_from_phy(phy, iw.INTERFACE_TYPE.ap,
interface_suffix)
if ap_interface:
return 'w%s%s' % (iw.INTERFACE_TYPE.client,
re.match(r'[^0-9]+([0-9]+)$', ap_interface).group(1))
else:
return None
@iw.requires_iw # Client mode not supported on Broadcom.
def set_client_wifi(opt):
"""Set up a wifi client in response to the 'setclient' command.
Args:
opt: The OptDict parsed from command line options.
Returns:
Whether wpa_supplicant successfully started and associated.
Raises:
BinWifiException: On various errors.
"""
if not opt.ssid:
raise utils.BinWifiException('You must specify an ssid with --ssid')
band = opt.band
if band not in ('2.4', '5'):
raise utils.BinWifiException('You must specify band with -b2.4 or -b5')
psk = os.environ.get('WIFI_CLIENT_PSK', None)
if band == '5' and quantenna.set_client_wifi(opt):
return True
phy = iw.find_phy(band, 'auto')
if phy is None:
utils.log("Couldn't find phy for band %s", band)
return False
interface = iw.find_interface_from_phy(phy, iw.INTERFACE_TYPE.client,
opt.interface_suffix)
if interface is None:
# Create the client interface if it does not exist, using the same number as
# an existing AP interface, which is stable across driver reloads.
interface = client_interface_name(phy, opt.interface_suffix)
if interface is None:
raise utils.BinWifiException('AP interface not initialized for %s' % phy)
if not iw.does_interface_exist(interface):
utils.log('Creating client interface %s', interface)
utils.subprocess_quiet(
('iw', 'phy', phy, 'interface', 'add', interface, 'type', 'station'),
no_stdout=True)
ap_mac_address = utils.get_mac_address_for_interface(
iw.find_interface_from_phy(
phy, iw.INTERFACE_TYPE.ap,
opt.interface_suffix))
mac_address = utils.increment_mac_address(ap_mac_address)
subprocess.check_call(
('ip', 'link', 'set', interface, 'address', mac_address))
wpa_config = configs.generate_wpa_supplicant_config(opt.ssid, psk, opt)
if not _set_wpa_supplicant_config(interface, wpa_config, opt):
return False
return True
@iw.requires_iw # Client mode not supported on Broadcom.
def stop_client_wifi(opt):
"""Disable a wifi client in response to the 'stopclient' command.
Args:
opt: The OptDict parsed from command line options.
Returns:
Whether wpa_supplicant was successfully stopped on all bands specified by
opt.band.
"""
success = True
for band in opt.band.split():
utils.log('stopping client for %s GHz...', band)
if band == '5' and quantenna.stop_client_wifi(opt):
continue
interface = iw.find_interface_from_band(
band, iw.INTERFACE_TYPE.client, opt.interface_suffix)
if interface is None:
utils.log('No client interface for %s GHz; nothing to stop', band)
continue
if _stop_wpa_supplicant(interface):
if opt.persist:
persist.delete_options('wpa_supplicant', band)
else:
utils.log('Failed to stop wpa_supplicant on interface %s', interface)
success = False
return success
def stringify_options(optdict):
for option in ('channel', 'width', 'band', 'ssid'):
optdict.__setitem__(option, str(optdict.__getitem__(option)))
def _run(argv):
"""Runs a wifi command.
This is the main entry point of the script, and is called by the main function
and also by commands which need to run other commands (e.g. restore).
Args:
argv: A list containing a command and a series of options, e.g.
sys.argv[1:].
Returns:
Whether the command succeeded.
Raises:
BinWifiException: On file write failures.
"""
global lockfile_taken
serial = 'UNKNOWN'
try:
serial = subprocess.check_output(('serial')).strip()
except subprocess.CalledProcessError:
utils.log('Could not get serial number')
optspec = _OPTSPEC_FORMAT.format(
bin=__file__.split('/')[-1], ssid='%s_TestWifi' % serial)
parser = options.Options(optspec)
opt, _, extra = parser.parse(argv)
stringify_options(opt)
if not extra:
parser.fatal('Must specify a command (see usage for details).')
return 1
# set and setclient have a different default for -b.
if extra[0].startswith('set') and ' ' in opt.band:
opt.band = '2.4'
try:
function = {
'set': set_wifi,
'stop': stop_wifi,
'off': stop_wifi,
'restore': restore_wifi,
'show': show_wifi,
'setclient': set_client_wifi,
'stopclient': stop_client_wifi,
'stopap': stop_ap_wifi,
'scan': scan_wifi,
}[extra[0]]
except KeyError:
parser.fatal('Unrecognized command %s' % extra[0])
if not lockfile_taken:
utils.lock(_LOCKFILE, int(opt.lock_timeout))
atexit.register(utils.unlock, _LOCKFILE)
lockfile_taken = True
success = function(opt)
if success:
if extra[0] in ('set', 'setclient'):
program = 'hostapd' if extra[0] == 'set' else 'wpa_supplicant'
if opt.persist:
phy = iw.find_phy(opt.band, opt.channel)
for band in iw.phy_bands(phy):
if band != opt.band:
persist.delete_options(program, band)
persist.save_options(program, opt.band, argv)
persist.save_options(program, opt.band, argv, tmp=True)
return success
def main():
try:
return 0 if _run(sys.argv[1:]) else 1
except iw.RequiresIwException as e:
utils.log('NOOP: %s', e)
return 0
except utils.BinWifiException as e:
utils.log('FATAL: %s', e)
return 99
except subprocess.CalledProcessError as e:
utils.log('FATAL: subprocess failed unexpectedly: %s', e)
return 99
if __name__ == '__main__':
sys.stdout = os.fdopen(1, 'w', 1) # force line buffering even if redirected
sys.stderr = os.fdopen(2, 'w', 1) # force line buffering even if redirected
sys.exit(main())