1111 lines
43 KiB
Python
1111 lines
43 KiB
Python
# 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.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)
|
|
|
|
return LinuxRouter(hosts.create_host(router_hostname), 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'
|
|
|
|
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').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().split(None, 2)[0]
|
|
logging.debug('Will only retrieve logs after %s.',
|
|
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
|
|
|
|
# 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)
|
|
|
|
|
|
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.
|
|
if configuration.min_streams is None:
|
|
interface = self.get_wlanif(configuration.frequency, 'managed')
|
|
else:
|
|
interface = self.get_wlanif(
|
|
configuration.frequency, 'managed', configuration.min_streams)
|
|
|
|
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, self.iw_runner.get_interface(interface).phy,
|
|
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.')
|
|
start_time = time.time()
|
|
while time.time() - start_time < self.STARTUP_TIMEOUT_SECONDS:
|
|
success = self.router.run(
|
|
'grep "Setup of interface done" %s' % log_file,
|
|
ignore_status=True).exit_status == 0
|
|
if success:
|
|
break
|
|
|
|
# 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.')
|
|
|
|
time.sleep(self.POLLING_INTERVAL_SECONDS)
|
|
else:
|
|
raise error.TestFail('Timed out while waiting for hostapd '
|
|
'to start.')
|
|
|
|
|
|
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)
|
|
is_dead = False
|
|
start_time = time.time()
|
|
while not is_dead and time.time() - start_time < timeout_seconds:
|
|
time.sleep(self.POLLING_INTERVAL_SECONDS)
|
|
is_dead = self.host.run(
|
|
'pgrep -l %s' % search_arg,
|
|
ignore_status=True).exit_status != 0
|
|
if is_dead or ignore_timeouts:
|
|
return is_dead
|
|
|
|
raise error.TestError(
|
|
'Timed out waiting for %s%s to die' %
|
|
(process,
|
|
'' if instance is None else ' (instance=%s)' % instance))
|
|
|
|
|
|
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 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')
|
|
|
|
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)
|
|
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_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):
|
|
"""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
|
|
|
|
"""
|
|
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['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))
|
|
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['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)
|
|
|
|
|
|
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 _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 setup_management_frame_interface(self, channel):
|
|
"""
|
|
Setup interface for injecting management frames.
|
|
|
|
@param channel int channel to inject the frames.
|
|
|
|
@return string name of the interface.
|
|
|
|
"""
|
|
frequency = hostap_config.HostapConfig.get_frequency_for_channel(
|
|
channel)
|
|
interface = self.get_wlanif(frequency, 'monitor')
|
|
self.router.run('%s link set %s up' % (self.cmd_ip, interface))
|
|
self.iw_runner.set_freq(interface, frequency)
|
|
return 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())
|