blob: fe8e86d273b0cd2523b174b78237490a03643e13 [file] [log] [blame]
# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import collections
import copy
import logging
import random
import string
import tempfile
import time
from autotest_lib.client.common_lib import error
from autotest_lib.client.common_lib import utils
from autotest_lib.client.common_lib.cros import path_utils
from autotest_lib.client.common_lib.cros.network import interface
from autotest_lib.client.common_lib.cros.network import netblock
from autotest_lib.client.common_lib.cros.network import ping_runner
from autotest_lib.server import hosts
from autotest_lib.server import site_linux_system
from autotest_lib.server.cros import dnsname_mangler
from autotest_lib.server.cros.network import hostap_config
StationInstance = collections.namedtuple('StationInstance',
['ssid', 'interface', 'dev_type'])
HostapdInstance = collections.namedtuple('HostapdInstance',
['ssid', 'conf_file', 'log_file',
'interface', 'config_dict',
'stderr_log_file',
'scenario_name'])
# Send magic packets here, so they can wake up the system but are otherwise
# dropped.
UDP_DISCARD_PORT = 9
def build_router_hostname(client_hostname=None, router_hostname=None):
"""Build a router hostname from a client hostname.
@param client_hostname: string hostname of DUT connected to a router.
@param router_hostname: string hostname of router.
@return string hostname of connected router or None if the hostname
cannot be inferred from the client hostname.
"""
if not router_hostname and not client_hostname:
raise error.TestError('Either client_hostname or router_hostname must '
'be specified to build_router_hostname.')
return dnsname_mangler.get_router_addr(client_hostname,
cmdline_override=router_hostname)
def build_router_proxy(test_name='', client_hostname=None, router_addr=None,
enable_avahi=False):
"""Build up a LinuxRouter object.
Verifies that the remote host responds to ping.
Either client_hostname or router_addr must be specified.
@param test_name: string name of this test (e.g. 'network_WiFi_TestName').
@param client_hostname: string hostname of DUT if we're in the lab.
@param router_addr: string DNS/IPv4 address to use for router host object.
@param enable_avahi: boolean True iff avahi should be started on the router.
@return LinuxRouter or raise error.TestError on failure.
"""
router_hostname = build_router_hostname(client_hostname=client_hostname,
router_hostname=router_addr)
logging.info('Connecting to router at %s', router_hostname)
ping_helper = ping_runner.PingRunner()
if not ping_helper.simple_ping(router_hostname):
raise error.TestError('Router at %s is not pingable.' %
router_hostname)
# Use CrosHost for all router hosts and avoid host detection.
# Host detection would use JetstreamHost for Whirlwind routers.
# JetstreamHost assumes ap-daemons are running.
# Testbed routers run the testbed-ap profile with no ap-daemons.
# TODO(ecgh): crbug.com/757075 Fix testbed-ap JetstreamHost detection.
return LinuxRouter(hosts.create_host(router_hostname,
host_class=hosts.CrosHost),
test_name,
enable_avahi=enable_avahi)
class LinuxRouter(site_linux_system.LinuxSystem):
"""Linux/mac80211-style WiFi Router support for WiFiTest class.
This class implements test methods/steps that communicate with a
router implemented with Linux/mac80211. The router must
be pre-configured to enable ssh access and have a mac80211-based
wireless device. We also assume hostapd 0.7.x and iw are present
and any necessary modules are pre-loaded.
"""
KNOWN_TEST_PREFIX = 'network_WiFi_'
POLLING_INTERVAL_SECONDS = 0.5
STARTUP_TIMEOUT_SECONDS = 10
SUFFIX_LETTERS = string.ascii_lowercase + string.digits
SUBNET_PREFIX_OCTETS = (192, 168)
HOSTAPD_CONF_FILE_PATTERN = '/tmp/hostapd-test-%s.conf'
HOSTAPD_LOG_FILE_PATTERN = '/tmp/hostapd-test-%s.log'
HOSTAPD_STDERR_LOG_FILE_PATTERN = '/tmp/hostapd-stderr-test-%s.log'
HOSTAPD_CONTROL_INTERFACE_PATTERN = '/tmp/hostapd-test-%s.ctrl'
HOSTAPD_DRIVER_NAME = 'nl80211'
STATION_CONF_FILE_PATTERN = '/tmp/wpa-supplicant-test-%s.conf'
STATION_LOG_FILE_PATTERN = '/tmp/wpa-supplicant-test-%s.log'
STATION_PID_FILE_PATTERN = '/tmp/wpa-supplicant-test-%s.pid'
MGMT_FRAME_SENDER_LOG_FILE = '/tmp/send_management_frame-test.log'
PROBE_RESPONSE_FOOTER_FILE = '/tmp/autotest-probe_response_footer'
_RNG_AVAILABLE = '/sys/class/misc/hw_random/rng_available'
_RNG_CURRENT = '/sys/class/misc/hw_random/rng_current'
def get_capabilities(self):
"""@return iterable object of AP capabilities for this system."""
caps = set()
try:
self.cmd_send_management_frame = path_utils.must_be_installed(
'/usr/bin/send_management_frame', host=self.host)
caps.add(self.CAPABILITY_SEND_MANAGEMENT_FRAME)
except error.TestFail:
pass
return super(LinuxRouter, self).get_capabilities().union(caps)
@property
def router(self):
"""Deprecated. Use self.host instead.
@return Host object representing the remote router.
"""
return self.host
@property
def wifi_ip(self):
"""Simple accessor for the WiFi IP when there is only one AP.
@return string IP of WiFi interface.
"""
if len(self.local_servers) != 1:
raise error.TestError('Could not pick a WiFi IP to return.')
return self.get_wifi_ip(0)
def __init__(self, host, test_name, enable_avahi=False):
"""Build a LinuxRouter.
@param host Host object representing the remote machine.
@param test_name string name of this test. Used in SSID creation.
@param enable_avahi: boolean True iff avahi should be started on the
router.
"""
super(LinuxRouter, self).__init__(host, 'router')
self._ssid_prefix = test_name
self._enable_avahi = enable_avahi
self.__setup()
def __setup(self):
"""Set up this system.
Can be used either to complete initialization of a LinuxRouter
object, or to re-establish a good state after a reboot.
"""
self.cmd_dhcpd = '/usr/sbin/dhcpd'
self.cmd_hostapd = path_utils.must_be_installed(
'/usr/sbin/hostapd', host=self.host)
self.cmd_hostapd_cli = path_utils.must_be_installed(
'/usr/sbin/hostapd_cli', host=self.host)
self.cmd_wpa_supplicant = path_utils.must_be_installed(
'/usr/sbin/wpa_supplicant', host=self.host)
self.dhcpd_conf = '/tmp/dhcpd.%s.conf'
self.dhcpd_leases = '/tmp/dhcpd.leases'
# Log the most recent message on the router so that we can rebuild the
# suffix relevant to us when debugging failures.
last_log_line = self.host.run('tail -1 /var/log/messages',
ignore_status=True).stdout
# We're trying to get the timestamp from:
# 2014-07-23T17:29:34.961056+00:00 localhost kernel: blah blah blah
self._log_start_timestamp = last_log_line.strip().partition(' ')[0]
if self._log_start_timestamp:
logging.debug('Will only retrieve logs after %s.',
self._log_start_timestamp)
else:
# If syslog is empty, we just use a wildcard pattern, to grab
# everything.
logging.debug('Empty or corrupt log; will retrieve whole log')
self._log_start_timestamp = '.'
# hostapd configuration persists throughout the test, subsequent
# 'config' commands only modify it.
if self._ssid_prefix.startswith(self.KNOWN_TEST_PREFIX):
# Many of our tests start with an uninteresting prefix.
# Remove it so we can have more unique bytes.
self._ssid_prefix = self._ssid_prefix[len(self.KNOWN_TEST_PREFIX):]
self._number_unique_ssids = 0
self._total_hostapd_instances = 0
self.local_servers = []
self.server_address_index = []
self.hostapd_instances = []
self.station_instances = []
self.dhcp_low = 1
self.dhcp_high = 128
# Tear down hostapbr bridge interfaces
result = self.host.run('ls -d /sys/class/net/%s*' %
self.HOSTAP_BRIDGE_INTERFACE_PREFIX,
ignore_status=True)
if result.exit_status == 0:
for path in result.stdout.splitlines():
self.delete_link(path.split('/')[-1])
# Kill hostapd and dhcp server if already running.
self._kill_process_instance('hostapd', timeout_seconds=30)
self.stop_dhcp_server(instance=None)
# Place us in the US by default
self.iw_runner.set_regulatory_domain('US')
self.enable_all_antennas()
# Some tests want this functionality, but otherwise, it's a distraction.
if self._enable_avahi:
self.host.run('start avahi', ignore_status=True)
else:
self.host.run('stop avahi', ignore_status=True)
# Some routers have bad (slow?) random number generators.
self.rng_configure()
def close(self):
"""Close global resources held by this system."""
self.deconfig()
# dnsmasq and hostapd cause interesting events to go to system logs.
# Retrieve only the suffix of the logs after the timestamp we stored on
# router creation.
self.host.run("sed -n -e '/%s/,$p' /var/log/messages >/tmp/router_log" %
self._log_start_timestamp, ignore_status=True)
self.host.get_file('/tmp/router_log', 'debug/router_host_messages')
super(LinuxRouter, self).close()
def reboot(self, timeout):
"""Reboot this router, and restore it to a known-good state.
@param timeout Maximum seconds to wait for router to return.
"""
super(LinuxRouter, self).reboot(timeout)
self.__setup()
def has_local_server(self):
"""@return True iff this router has local servers configured."""
return bool(self.local_servers)
def start_hostapd(self, configuration):
"""Start a hostapd instance described by conf.
@param configuration HostapConfig object.
"""
# Figure out the correct interface.
interface = self.get_wlanif(configuration.frequency, 'managed',
configuration.min_streams)
phy_name = self.iw_runner.get_interface(interface).phy
conf_file = self.HOSTAPD_CONF_FILE_PATTERN % interface
log_file = self.HOSTAPD_LOG_FILE_PATTERN % interface
stderr_log_file = self.HOSTAPD_STDERR_LOG_FILE_PATTERN % interface
control_interface = self.HOSTAPD_CONTROL_INTERFACE_PATTERN % interface
hostapd_conf_dict = configuration.generate_dict(
interface, control_interface,
self.build_unique_ssid(suffix=configuration.ssid_suffix))
logging.debug('hostapd parameters: %r', hostapd_conf_dict)
# Generate hostapd.conf.
self.router.run("cat <<EOF >%s\n%s\nEOF\n" %
(conf_file, '\n'.join(
"%s=%s" % kv for kv in hostapd_conf_dict.iteritems())))
# Run hostapd.
logging.info('Starting hostapd on %s(%s) channel=%s...',
interface, phy_name, configuration.channel)
self.router.run('rm %s' % log_file, ignore_status=True)
self.router.run('stop wpasupplicant', ignore_status=True)
start_command = '%s -dd -t %s > %s 2> %s & echo $!' % (
self.cmd_hostapd, conf_file, log_file, stderr_log_file)
pid = int(self.router.run(start_command).stdout.strip())
self.hostapd_instances.append(HostapdInstance(
hostapd_conf_dict['ssid'],
conf_file,
log_file,
interface,
hostapd_conf_dict.copy(),
stderr_log_file,
configuration.scenario_name))
# Wait for confirmation that the router came up.
logging.info('Waiting for hostapd to startup.')
utils.poll_for_condition(
condition=lambda: self._has_hostapd_started(log_file, pid),
exception=error.TestFail('Timed out while waiting for hostapd '
'to start.'),
timeout=self.STARTUP_TIMEOUT_SECONDS,
sleep_interval=self.POLLING_INTERVAL_SECONDS)
if configuration.frag_threshold:
threshold = self.iw_runner.get_fragmentation_threshold(phy_name)
if threshold != configuration.frag_threshold:
raise error.TestNAError('Router does not support setting '
'fragmentation threshold')
def _has_hostapd_started(self, log_file, pid):
"""Determines if hostapd has started.
@return Whether or not hostapd has started.
@raise error.TestFail if there was a bad config or hostapd terminated.
"""
success = self.router.run(
'grep "Setup of interface done" %s' % log_file,
ignore_status=True).exit_status == 0
if success:
return True
# A common failure is an invalid router configuration.
# Detect this and exit early if we see it.
bad_config = self.router.run(
'grep "Interface initialization failed" %s' % log_file,
ignore_status=True).exit_status == 0
if bad_config:
raise error.TestFail('hostapd failed to initialize AP '
'interface.')
if pid:
early_exit = self.router.run('kill -0 %d' % pid,
ignore_status=True).exit_status
if early_exit:
raise error.TestFail('hostapd process terminated.')
return False
def _kill_process_instance(self,
process,
instance=None,
timeout_seconds=10,
ignore_timeouts=False):
"""Kill a process on the router.
Kills remote program named |process| (optionally only a specific
|instance|). Wait |timeout_seconds| for |process| to die
before returning. If |ignore_timeouts| is False, raise
a TestError on timeouts.
@param process: string name of process to kill.
@param instance: string fragment of the command line unique to
this instance of the remote process.
@param timeout_seconds: float timeout in seconds to wait.
@param ignore_timeouts: True iff we should ignore failures to
kill processes.
@return True iff the specified process has exited.
"""
if instance is not None:
search_arg = '-f "^%s.*%s"' % (process, instance)
else:
search_arg = process
self.host.run('pkill %s' % search_arg, ignore_status=True)
# Wait for process to die
time.sleep(self.POLLING_INTERVAL_SECONDS)
try:
utils.poll_for_condition(
condition=lambda: self.host.run(
'pgrep -l %s' % search_arg,
ignore_status=True).exit_status != 0,
timeout=timeout_seconds,
sleep_interval=self.POLLING_INTERVAL_SECONDS)
except utils.TimeoutError:
if ignore_timeouts:
return False
raise error.TestError(
'Timed out waiting for %s%s to die' %
(process,
'' if instance is None else ' (instance=%s)' % instance))
return True
def kill_hostapd_instance(self, instance):
"""Kills a hostapd instance.
@param instance HostapdInstance object.
"""
is_dead = self._kill_process_instance(
self.cmd_hostapd,
instance=instance.conf_file,
timeout_seconds=30,
ignore_timeouts=True)
if instance.scenario_name:
log_identifier = instance.scenario_name
else:
log_identifier = '%d_%s' % (
self._total_hostapd_instances, instance.interface)
files_to_copy = [(instance.log_file,
'debug/hostapd_router_%s.log' % log_identifier),
(instance.stderr_log_file,
'debug/hostapd_router_%s.stderr.log' %
log_identifier)]
for remote_file, local_file in files_to_copy:
if self.host.run('ls %s >/dev/null 2>&1' % remote_file,
ignore_status=True).exit_status:
logging.error('Did not collect hostapd log file because '
'it was missing.')
else:
self.router.get_file(remote_file, local_file)
self._total_hostapd_instances += 1
if not is_dead:
raise error.TestError('Timed out killing hostapd.')
def build_unique_ssid(self, suffix=''):
""" Build our unique token by base-<len(self.SUFFIX_LETTERS)> encoding
the number of APs we've constructed already.
@param suffix string to append to SSID
"""
base = len(self.SUFFIX_LETTERS)
number = self._number_unique_ssids
self._number_unique_ssids += 1
unique = ''
while number or not unique:
unique = self.SUFFIX_LETTERS[number % base] + unique
number = number / base
# And salt the SSID so that tests running in adjacent cells are unlikely
# to pick the same SSID and we're resistent to beacons leaking out of
# cells.
salt = ''.join([random.choice(self.SUFFIX_LETTERS) for x in range(5)])
return '_'.join([self._ssid_prefix, unique, salt, suffix])[-32:]
def rng_configure(self):
"""Configure the random generator to our liking.
Some routers (particularly, Gale) seem to have bad Random Number
Generators, such that hostapd can't always generate keys fast enough.
The on-board TPM seems to serve as a better generator, so we try to
switch to that if available.
Symptoms of a slow RNG: hostapd complains with:
WPA: Not enough entropy in random pool to proceed - reject first
4-way handshake
Ref:
https://chromium.googlesource.com/chromiumos/third_party/hostap/+/7ea51f728bb7/src/ap/wpa_auth.c#1854
Linux devices may have RNG parameters at
/sys/class/misc/hw_random/rng_{available,current}. See:
https://www.kernel.org/doc/Documentation/hw_random.txt
"""
available = self.host.run('cat %s' % self._RNG_AVAILABLE, \
ignore_status=True).stdout.strip().split(' ')
# System may not have HWRNG support. Just skip this.
if available == "":
return
current = self.host.run('cat %s' % self._RNG_CURRENT).stdout. \
strip()
want_rng = "tpm-rng"
logging.info("Available / current RNGs on router: %r / %s",
available, current)
if want_rng in available and want_rng != current:
logging.info("Switching RNGs: %s -> %s", current, want_rng)
self.host.run('echo -n "%s" > %s' % (want_rng, self._RNG_CURRENT))
def hostap_configure(self, configuration, multi_interface=None):
"""Build up a hostapd configuration file and start hostapd.
Also setup a local server if this router supports them.
@param configuration HosetapConfig object.
@param multi_interface bool True iff multiple interfaces allowed.
"""
if multi_interface is None and (self.hostapd_instances or
self.station_instances):
self.deconfig()
if configuration.is_11ac:
router_caps = self.get_capabilities()
if site_linux_system.LinuxSystem.CAPABILITY_VHT not in router_caps:
raise error.TestNAError('Router does not have AC support')
if configuration.use_bridge:
configuration._bridge = self.get_brif()
self.start_hostapd(configuration)
interface = self.hostapd_instances[-1].interface
self.iw_runner.set_tx_power(interface, 'auto')
self.set_beacon_footer(interface, configuration.beacon_footer)
self.start_local_server(interface, bridge=configuration.bridge)
logging.info('AP configured.')
def ibss_configure(self, config):
"""Configure a station based AP in IBSS mode.
Extract relevant configuration objects from |config| despite not
actually being a hostap managed endpoint.
@param config HostapConfig object.
"""
if self.station_instances or self.hostapd_instances:
self.deconfig()
interface = self.get_wlanif(config.frequency, 'ibss')
ssid = (config.ssid or
self.build_unique_ssid(suffix=config.ssid_suffix))
# Connect the station
self.router.run('%s link set %s up' % (self.cmd_ip, interface))
self.iw_runner.ibss_join(interface, ssid, config.frequency)
# Always start a local server.
self.start_local_server(interface)
# Remember that this interface is up.
self.station_instances.append(
StationInstance(ssid=ssid, interface=interface,
dev_type='ibss'))
def local_server_address(self, index):
"""Get the local server address for an interface.
When we multiple local servers, we give them static IP addresses
like 192.168.*.254.
@param index int describing which local server this is for.
"""
return '%d.%d.%d.%d' % (self.SUBNET_PREFIX_OCTETS + (index, 254))
def local_peer_ip_address(self, index):
"""Get the IP address allocated for the peer associated to the AP.
This address is assigned to a locally associated peer device that
is created for the DUT to perform connectivity tests with.
When we have multiple local servers, we give them static IP addresses
like 192.168.*.253.
@param index int describing which local server this is for.
"""
return '%d.%d.%d.%d' % (self.SUBNET_PREFIX_OCTETS + (index, 253))
def local_bridge_address(self, index):
"""Get the bridge address for an interface.
This address is assigned to a local bridge device.
@param index int describing which local server this is for.
"""
return '%d.%d.%d.%d' % (self.SUBNET_PREFIX_OCTETS + (index, 252))
def local_peer_mac_address(self):
"""Get the MAC address of the peer interface.
@return string MAC address of the peer interface.
"""
iface = interface.Interface(self.station_instances[0].interface,
self.router)
return iface.mac_address
def _get_unused_server_address_index(self):
"""@return an unused server address index."""
for address_index in range(0, 256):
if address_index not in self.server_address_index:
return address_index
raise error.TestFail('No available server address index')
def change_server_address_index(self, ap_num=0, server_address_index=None):
"""Restart the local server with a different server address index.
This will restart the local server with different gateway IP address
and DHCP address ranges.
@param ap_num: int hostapd instance number.
@param server_address_index: int server address index.
"""
interface = self.local_servers[ap_num]['interface'];
# Get an unused server address index if one is not specified, which
# will be different from the one that's currently in used.
if server_address_index is None:
server_address_index = self._get_unused_server_address_index()
# Restart local server with the new server address index.
self.stop_local_server(self.local_servers[ap_num])
self.start_local_server(interface,
ap_num=ap_num,
server_address_index=server_address_index)
def start_local_server(self,
interface,
ap_num=None,
server_address_index=None,
bridge=None):
"""Start a local server on an interface.
@param interface string (e.g. wlan0)
@param ap_num int the ap instance to start the server for
@param server_address_index int server address index
@param bridge string (e.g. br0)
"""
logging.info('Starting up local server...')
if len(self.local_servers) >= 256:
raise error.TestFail('Exhausted available local servers')
# Get an unused server address index if one is not specified.
# Validate server address index if one is specified.
if server_address_index is None:
server_address_index = self._get_unused_server_address_index()
elif server_address_index in self.server_address_index:
raise error.TestFail('Server address index %d already in used' %
server_address_index)
server_addr = netblock.from_addr(
self.local_server_address(server_address_index),
prefix_len=24)
params = {}
params['address_index'] = server_address_index
params['netblock'] = server_addr
params['dhcp_range'] = ' '.join(
(server_addr.get_addr_in_block(1),
server_addr.get_addr_in_block(128)))
params['interface'] = interface
params['bridge'] = bridge
params['ip_params'] = ('%s broadcast %s dev %s' %
(server_addr.netblock,
server_addr.broadcast,
interface))
if ap_num is None:
self.local_servers.append(params)
else:
self.local_servers.insert(ap_num, params)
self.server_address_index.append(server_address_index)
self.router.run('%s addr flush %s' %
(self.cmd_ip, interface))
self.router.run('%s addr add %s' %
(self.cmd_ip, params['ip_params']))
self.router.run('%s link set %s up' %
(self.cmd_ip, interface))
if params['bridge']:
bridge_addr = netblock.from_addr(
self.local_bridge_address(server_address_index),
prefix_len=24)
self.router.run("ifconfig %s %s" %
(params['bridge'], bridge_addr.netblock))
self.start_dhcp_server(interface)
def stop_local_server(self, server):
"""Stop a local server on the router
@param server object server configuration parameters.
"""
self.stop_dhcp_server(server['interface'])
self.router.run("%s addr del %s" %
(self.cmd_ip, server['ip_params']),
ignore_status=True)
self.server_address_index.remove(server['address_index'])
self.local_servers.remove(server)
def start_dhcp_server(self, interface):
"""Start a dhcp server on an interface.
@param interface string (e.g. wlan0)
"""
for server in self.local_servers:
if server['interface'] == interface:
params = server
break
else:
raise error.TestFail('Could not find local server '
'to match interface: %r' % interface)
server_addr = params['netblock']
dhcpd_conf_file = self.dhcpd_conf % interface
dhcp_conf = '\n'.join([
'port=0', # disables DNS server
'bind-interfaces',
'log-dhcp',
'dhcp-range=%s' % ','.join((server_addr.get_addr_in_block(1),
server_addr.get_addr_in_block(128))),
'interface=%s' % (params['bridge'] or params['interface']),
'dhcp-leasefile=%s' % self.dhcpd_leases])
self.router.run('cat <<EOF >%s\n%s\nEOF\n' %
(dhcpd_conf_file, dhcp_conf))
self.router.run('dnsmasq --conf-file=%s' % dhcpd_conf_file)
def stop_dhcp_server(self, instance=None):
"""Stop a dhcp server on the router.
@param instance string instance to kill.
"""
self._kill_process_instance('dnsmasq', instance=instance)
def get_wifi_channel(self, ap_num):
"""Return channel of BSS corresponding to |ap_num|.
@param ap_num int which BSS to get the channel of.
@return int primary channel of BSS.
"""
instance = self.hostapd_instances[ap_num]
return instance.config_dict['channel']
def get_wifi_ip(self, ap_num):
"""Return IP address on the WiFi subnet of a local server on the router.
If no local servers are configured (e.g. for an RSPro), a TestFail will
be raised.
@param ap_num int which local server to get an address from.
"""
if not self.local_servers:
raise error.TestError('No IP address assigned')
return self.local_servers[ap_num]['netblock'].addr
def get_wifi_ip_subnet(self, ap_num):
"""Return subnet of WiFi AP instance.
If no APs are configured a TestError will be raised.
@param ap_num int which local server to get an address from.
"""
if not self.local_servers:
raise error.TestError('No APs configured.')
return self.local_servers[ap_num]['netblock'].subnet
def get_hostapd_interface(self, ap_num):
"""Get the name of the interface associated with a hostapd instance.
@param ap_num: int hostapd instance number.
@return string interface name (e.g. 'managed0').
"""
if ap_num not in range(len(self.hostapd_instances)):
raise error.TestFail('Invalid instance number (%d) with %d '
'instances configured.' %
(ap_num, len(self.hostapd_instances)))
instance = self.hostapd_instances[ap_num]
return instance.interface
def get_station_interface(self, instance):
"""Get the name of the interface associated with a station.
@param instance: int station instance number.
@return string interface name (e.g. 'managed0').
"""
if instance not in range(len(self.station_instances)):
raise error.TestFail('Invalid instance number (%d) with %d '
'instances configured.' %
(instance, len(self.station_instances)))
instance = self.station_instances[instance]
return instance.interface
def get_hostapd_mac(self, ap_num):
"""Return the MAC address of an AP in the test.
@param ap_num int index of local server to read the MAC address from.
@return string MAC address like 00:11:22:33:44:55.
"""
interface_name = self.get_hostapd_interface(ap_num)
ap_interface = interface.Interface(interface_name, self.host)
return ap_interface.mac_address
def get_hostapd_phy(self, ap_num):
"""Get name of phy for hostapd instance.
@param ap_num int index of hostapd instance.
@return string phy name of phy corresponding to hostapd's
managed interface.
"""
interface = self.iw_runner.get_interface(
self.get_hostapd_interface(ap_num))
return interface.phy
def deconfig(self):
"""A legacy, deprecated alias for deconfig_aps."""
self.deconfig_aps()
def deconfig_aps(self, instance=None, silent=False):
"""De-configure an AP (will also bring wlan down).
@param instance: int or None. If instance is None, will bring down all
instances of hostapd.
@param silent: True if instances should be brought without de-authing
the DUT.
"""
if not self.hostapd_instances and not self.station_instances:
return
if self.hostapd_instances:
local_servers = []
if instance is not None:
instances = [ self.hostapd_instances.pop(instance) ]
for server in self.local_servers:
if server['interface'] == instances[0].interface:
local_servers = [server]
break
else:
instances = self.hostapd_instances
self.hostapd_instances = []
local_servers = copy.copy(self.local_servers)
for instance in instances:
if silent:
# Deconfigure without notifying DUT. Remove the interface
# hostapd uses to send beacon and DEAUTH packets.
self.remove_interface(instance.interface)
self.kill_hostapd_instance(instance)
self.release_interface(instance.interface)
if self.station_instances:
local_servers = copy.copy(self.local_servers)
instance = self.station_instances.pop()
if instance.dev_type == 'ibss':
self.iw_runner.ibss_leave(instance.interface)
elif instance.dev_type == 'managed':
self._kill_process_instance(self.cmd_wpa_supplicant,
instance=instance.interface)
else:
self.iw_runner.disconnect_station(instance.interface)
self.router.run('%s link set %s down' %
(self.cmd_ip, instance.interface))
for server in local_servers:
self.stop_local_server(server)
for brif in range(self._brif_index):
self.delete_link('%s%d' %
(self.HOSTAP_BRIDGE_INTERFACE_PREFIX, brif))
def delete_link(self, name):
"""Delete link using the `ip` command.
@param name string link name.
"""
self.host.run('%s link del %s' % (self.cmd_ip, name),
ignore_status=True)
def set_ap_interface_down(self, instance=0):
"""Bring down the hostapd interface.
@param instance int router instance number.
"""
self.host.run('%s link set %s down' %
(self.cmd_ip, self.get_hostapd_interface(instance)))
def confirm_pmksa_cache_use(self, instance=0):
"""Verify that the PMKSA auth was cached on a hostapd instance.
@param instance int router instance number.
"""
log_file = self.hostapd_instances[instance].log_file
pmksa_match = 'PMK from PMKSA cache'
result = self.router.run('grep -q "%s" %s' % (pmksa_match, log_file),
ignore_status=True)
if result.exit_status:
raise error.TestFail('PMKSA cache was not used in roaming.')
def get_ssid(self, instance=None):
"""@return string ssid for the network stemming from this router."""
if instance is None:
instance = 0
if len(self.hostapd_instances) > 1:
raise error.TestFail('No instance of hostapd specified with '
'multiple instances present.')
if self.hostapd_instances:
return self.hostapd_instances[instance].ssid
if self.station_instances:
return self.station_instances[0].ssid
raise error.TestFail('Requested ssid of an unconfigured AP.')
def deauth_client(self, client_mac):
"""Deauthenticates a client described in params.
@param client_mac string containing the mac address of the client to be
deauthenticated.
"""
control_if = self.hostapd_instances[-1].config_dict['ctrl_interface']
self.router.run('%s -p%s deauthenticate %s' %
(self.cmd_hostapd_cli, control_if, client_mac))
def send_bss_tm_req(self, client_mac, neighbor_list):
"""Send a BSS Transition Management Request to a client.
@param client_mac string containing the mac address of the client.
@param neighbor_list list of strings containing mac addresses of
candidate APs.
@return bool True if BSS_TM_REQ is sent successfully.
"""
control_if = self.hostapd_instances[0].config_dict['ctrl_interface']
command = ('%s -p%s BSS_TM_REQ %s neighbor=%s,0,0,0,0 pref=1' %
(self.cmd_hostapd_cli, control_if, client_mac,
',0,0,0,0 neighbor='.join(neighbor_list)))
ret = self.router.run(command).stdout
if ret.splitlines()[-1] != 'OK':
return False
return True
def _prep_probe_response_footer(self, footer):
"""Write probe response footer temporarily to a local file and copy
over to test router.
@param footer string containing bytes for the probe response footer.
@raises AutoservRunError: If footer file copy fails.
"""
with tempfile.NamedTemporaryFile() as fp:
fp.write(footer)
fp.flush()
try:
self.host.send_file(fp.name, self.PROBE_RESPONSE_FOOTER_FILE)
except error.AutoservRunError:
logging.error('failed to copy footer file to AP')
raise
def send_management_frame_on_ap(self, frame_type, channel, instance=0):
"""Injects a management frame into an active hostapd session.
@param frame_type string the type of frame to send.
@param channel int targeted channel
@param instance int indicating which hostapd instance to inject into.
"""
hostap_interface = self.hostapd_instances[instance].interface
interface = self.get_wlanif(0, 'monitor', same_phy_as=hostap_interface)
self.router.run("%s link set %s up" % (self.cmd_ip, interface))
self.router.run('%s -i %s -t %s -c %d' %
(self.cmd_send_management_frame, interface, frame_type,
channel))
self.release_interface(interface)
def send_management_frame(self, interface, frame_type, channel,
ssid_prefix=None, num_bss=None,
frame_count=None, delay=None,
dest_addr=None, probe_resp_footer=None):
"""
Injects management frames on specify channel |frequency|.
This function will spawn off a new process to inject specified
management frames |frame_type| at the specified interface |interface|.
@param interface string interface to inject frames.
@param frame_type string message type.
@param channel int targeted channel.
@param ssid_prefix string SSID prefix.
@param num_bss int number of BSS.
@param frame_count int number of frames to send.
@param delay int milliseconds delay between frames.
@param dest_addr string destination address (DA) MAC address.
@param probe_resp_footer string footer for probe response.
@return int PID of the newly created process.
"""
command = '%s -i %s -t %s -c %d' % (self.cmd_send_management_frame,
interface, frame_type, channel)
if ssid_prefix is not None:
command += ' -s %s' % (ssid_prefix)
if num_bss is not None:
command += ' -b %d' % (num_bss)
if frame_count is not None:
command += ' -n %d' % (frame_count)
if delay is not None:
command += ' -d %d' % (delay)
if dest_addr is not None:
command += ' -a %s' % (dest_addr)
if probe_resp_footer is not None:
self._prep_probe_response_footer(footer=probe_resp_footer)
command += ' -f %s' % (self.PROBE_RESPONSE_FOOTER_FILE)
command += ' > %s 2>&1 & echo $!' % (self.MGMT_FRAME_SENDER_LOG_FILE)
pid = int(self.router.run(command).stdout)
return pid
def detect_client_deauth(self, client_mac, instance=0):
"""Detects whether hostapd has logged a deauthentication from
|client_mac|.
@param client_mac string the MAC address of the client to detect.
@param instance int indicating which hostapd instance to query.
"""
interface = self.hostapd_instances[instance].interface
deauth_msg = "%s: deauthentication: STA=%s" % (interface, client_mac)
log_file = self.hostapd_instances[instance].log_file
result = self.router.run("grep -qi '%s' %s" % (deauth_msg, log_file),
ignore_status=True)
return result.exit_status == 0
def detect_client_coexistence_report(self, client_mac, instance=0):
"""Detects whether hostapd has logged an action frame from
|client_mac| indicating information about 20/40MHz BSS coexistence.
@param client_mac string the MAC address of the client to detect.
@param instance int indicating which hostapd instance to query.
"""
coex_msg = ('nl80211: MLME event frame - hexdump(len=.*): '
'.. .. .. .. .. .. .. .. .. .. %s '
'.. .. .. .. .. .. .. .. 04 00.*48 01 ..' %
' '.join(client_mac.split(':')))
log_file = self.hostapd_instances[instance].log_file
result = self.router.run("grep -qi '%s' %s" % (coex_msg, log_file),
ignore_status=True)
return result.exit_status == 0
def add_connected_peer(self, instance=0):
"""Configure a station connected to a running AP instance.
Extract relevant configuration objects from the hostap
configuration for |instance| and generate a wpa_supplicant
instance that connects to it. This allows the DUT to interact
with a client entity that is also connected to the same AP. A
full wpa_supplicant instance is necessary here (instead of just
using the "iw" command to connect) since we want to enable
advanced features such as TDLS.
@param instance int indicating which hostapd instance to connect to.
"""
if not self.hostapd_instances:
raise error.TestFail('Hostapd is not configured.')
if self.station_instances:
raise error.TestFail('Station is already configured.')
ssid = self.get_ssid(instance)
hostap_conf = self.hostapd_instances[instance].config_dict
frequency = hostap_config.HostapConfig.get_frequency_for_channel(
hostap_conf['channel'])
self.configure_managed_station(
ssid, frequency, self.local_peer_ip_address(instance))
interface = self.station_instances[0].interface
# Since we now have two network interfaces connected to the same
# network, we need to disable the kernel's protection against
# incoming packets to an "unexpected" interface.
self.router.run('echo 2 > /proc/sys/net/ipv4/conf/%s/rp_filter' %
interface)
# Similarly, we'd like to prevent the hostap interface from
# replying to ARP requests for the peer IP address and vice
# versa.
self.router.run('echo 1 > /proc/sys/net/ipv4/conf/%s/arp_ignore' %
interface)
self.router.run('echo 1 > /proc/sys/net/ipv4/conf/%s/arp_ignore' %
hostap_conf['interface'])
def configure_managed_station(self, ssid, frequency, ip_addr):
"""Configure a router interface to connect as a client to a network.
@param ssid: string SSID of network to join.
@param frequency: int frequency required to join the network.
@param ip_addr: IP address to assign to this interface
(e.g. '192.168.1.200').
"""
interface = self.get_wlanif(frequency, 'managed')
# TODO(pstew): Configure other bits like PSK, 802.11n if tests
# require them...
supplicant_config = (
'network={\n'
' ssid="%(ssid)s"\n'
' key_mgmt=NONE\n'
'}\n' % {'ssid': ssid}
)
conf_file = self.STATION_CONF_FILE_PATTERN % interface
log_file = self.STATION_LOG_FILE_PATTERN % interface
pid_file = self.STATION_PID_FILE_PATTERN % interface
self.router.run('cat <<EOF >%s\n%s\nEOF\n' %
(conf_file, supplicant_config))
# Connect the station.
self.router.run('%s link set %s up' % (self.cmd_ip, interface))
start_command = ('%s -dd -t -i%s -P%s -c%s -D%s >%s 2>&1 &' %
(self.cmd_wpa_supplicant,
interface, pid_file, conf_file,
self.HOSTAPD_DRIVER_NAME, log_file))
self.router.run(start_command)
self.iw_runner.wait_for_link(interface)
# Assign an IP address to this interface.
self.router.run('%s addr add %s/24 dev %s' %
(self.cmd_ip, ip_addr, interface))
self.station_instances.append(
StationInstance(ssid=ssid, interface=interface,
dev_type='managed'))
def send_magic_packet(self, dest_ip, dest_mac):
"""Sends a magic packet to the NIC with the given IP and MAC addresses.
@param dest_ip the IP address of the device to send the packet to
@param dest_mac the hardware MAC address of the device
"""
# magic packet is 6 0xff bytes followed by the hardware address
# 16 times
mac_bytes = ''.join([chr(int(b, 16)) for b in dest_mac.split(':')])
magic_packet = '\xff' * 6 + mac_bytes * 16
logging.info('Sending magic packet to %s...', dest_ip)
self.host.run('python -uc "import socket, sys;'
's = socket.socket(socket.AF_INET, socket.SOCK_DGRAM);'
's.sendto(sys.stdin.read(), (\'%s\', %d))"' %
(dest_ip, UDP_DISCARD_PORT),
stdin=magic_packet)
def set_beacon_footer(self, interface, footer=''):
"""Sets the beacon footer (appended IE information) for this interface.
@param interface string interface to set the footer on.
@param footer string footer to be set on the interface.
"""
footer_file = ('/sys/kernel/debug/ieee80211/%s/beacon_footer' %
self.iw_runner.get_interface(interface).phy)
if self.router.run('test -e %s' % footer_file,
ignore_status=True).exit_status != 0:
logging.info('Beacon footer file does not exist. Ignoring.')
return
self.host.run('echo -ne %s > %s' % ('%r' % footer, footer_file))
def setup_bridge_mode_dhcp_server(self):
"""Setup an DHCP server for bridge mode.
Setup an DHCP server on the master interface of the virtual ethernet
pair, with peer interface connected to the bridge interface. This is
used for testing APs in bridge mode.
"""
# Start a local server on master interface of virtual ethernet pair.
self.start_local_server(
self.get_virtual_ethernet_master_interface())
# Add peer interface to the bridge.
self.add_interface_to_bridge(
self.get_virtual_ethernet_peer_interface())