You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1135 lines
44 KiB
1135 lines
44 KiB
# Lint as: python2, python3
|
|
# 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.
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import print_function
|
|
|
|
import collections
|
|
import copy
|
|
import logging
|
|
import os
|
|
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
|
|
import six
|
|
from six.moves import range
|
|
|
|
|
|
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 = 30
|
|
SUFFIX_LETTERS = string.ascii_lowercase + string.digits
|
|
SUBNET_PREFIX_OCTETS = (192, 168)
|
|
|
|
HOSTAPD_CONF_FILE_PATTERN = 'hostapd-test-%s.conf'
|
|
HOSTAPD_LOG_FILE_PATTERN = 'hostapd-test-%s.log'
|
|
HOSTAPD_STDERR_LOG_FILE_PATTERN = 'hostapd-stderr-test-%s.log'
|
|
HOSTAPD_CONTROL_INTERFACE_PATTERN = 'hostapd-test-%s.ctrl'
|
|
HOSTAPD_DRIVER_NAME = 'nl80211'
|
|
|
|
MGMT_FRAME_SENDER_LOG_FILE = '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, role='router'):
|
|
"""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.
|
|
@param role string description of host (e.g. router, pcap)
|
|
|
|
"""
|
|
super(LinuxRouter, self).__init__(host, role)
|
|
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 = os.path.join(self.logdir, 'dhcpd.%s.conf')
|
|
self.dhcpd_leases = os.path.join(self.logdir, '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._brif_index = 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)
|
|
|
|
# Some routers have bad (slow?) random number generators.
|
|
self.rng_configure()
|
|
|
|
|
|
def close(self):
|
|
"""Close global resources held by this system."""
|
|
router_log = os.path.join(self.logdir, 'router_log')
|
|
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 >%s" %
|
|
(self._log_start_timestamp, router_log),
|
|
ignore_status=True)
|
|
self.host.get_file(router_log, 'debug/%s_host_messages' % self.role)
|
|
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 = os.path.join(self.logdir,
|
|
self.HOSTAPD_CONF_FILE_PATTERN % interface)
|
|
log_file = os.path.join(self.logdir,
|
|
self.HOSTAPD_LOG_FILE_PATTERN % interface)
|
|
stderr_log_file = os.path.join(self.logdir,
|
|
self.HOSTAPD_STDERR_LOG_FILE_PATTERN % interface)
|
|
control_interface = os.path.join(self.logdir,
|
|
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 six.iteritems(hostapd_conf_dict))))
|
|
|
|
# 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 = (
|
|
'OPENSSL_CONF=/etc/ssl/openssl.cnf.compat '
|
|
'OPENSSL_CHROMIUM_SKIP_TRUSTED_PURPOSE_CHECK=1 '
|
|
'%s -dd -t -K %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_%s_%s.log' %
|
|
(self.role, log_identifier)),
|
|
(instance.stderr_log_file,
|
|
'debug/hostapd_%s_%s.stderr.log' %
|
|
(self.role, 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.debug("Available / current RNGs on router: %r / %s",
|
|
available, current)
|
|
if want_rng in available and want_rng != current:
|
|
logging.debug("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')
|
|
|
|
self.start_hostapd(configuration)
|
|
interface = self.hostapd_instances[-1].interface
|
|
self.iw_runner.set_tx_power(interface, 'auto')
|
|
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 list(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 list(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 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 string reply received from command
|
|
|
|
"""
|
|
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
|
|
return ret.splitlines()[-1]
|
|
|
|
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 $!' % (os.path.join(self.logdir,
|
|
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 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 setup_bridge_mode_dhcp_server(self):
|
|
"""Setup an DHCP server for bridge mode.
|
|
|
|
Setup an DHCP server on the main 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 main interface of virtual ethernet pair.
|
|
self.start_local_server(
|
|
self.get_virtual_ethernet_main_interface())
|
|
# Add peer interface to the bridge.
|
|
self.add_interface_to_bridge(
|
|
self.get_virtual_ethernet_peer_interface())
|
|
|
|
|
|
def create_brif(self):
|
|
"""Initialize a new bridge interface
|
|
|
|
@return string bridge interface name
|
|
"""
|
|
brif_name = '%s%d' % (self.HOSTAP_BRIDGE_INTERFACE_PREFIX,
|
|
self._brif_index)
|
|
self._brif_index += 1
|
|
self.host.run('brctl addbr %s' % brif_name)
|
|
return brif_name
|