Home | History | Annotate | Download | only in server
      1 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import collections
      6 import copy
      7 import logging
      8 import random
      9 import string
     10 import tempfile
     11 import time
     12 
     13 from autotest_lib.client.common_lib import error
     14 from autotest_lib.client.common_lib.cros import path_utils
     15 from autotest_lib.client.common_lib.cros.network import interface
     16 from autotest_lib.client.common_lib.cros.network import netblock
     17 from autotest_lib.client.common_lib.cros.network import ping_runner
     18 from autotest_lib.server import hosts
     19 from autotest_lib.server import site_linux_system
     20 from autotest_lib.server.cros import dnsname_mangler
     21 from autotest_lib.server.cros.network import hostap_config
     22 
     23 
     24 StationInstance = collections.namedtuple('StationInstance',
     25                                          ['ssid', 'interface', 'dev_type'])
     26 HostapdInstance = collections.namedtuple('HostapdInstance',
     27                                          ['ssid', 'conf_file', 'log_file',
     28                                           'interface', 'config_dict',
     29                                           'stderr_log_file',
     30                                           'scenario_name'])
     31 
     32 # Send magic packets here, so they can wake up the system but are otherwise
     33 # dropped.
     34 UDP_DISCARD_PORT = 9
     35 
     36 def build_router_hostname(client_hostname=None, router_hostname=None):
     37     """Build a router hostname from a client hostname.
     38 
     39     @param client_hostname: string hostname of DUT connected to a router.
     40     @param router_hostname: string hostname of router.
     41     @return string hostname of connected router or None if the hostname
     42             cannot be inferred from the client hostname.
     43 
     44     """
     45     if not router_hostname and not client_hostname:
     46         raise error.TestError('Either client_hostname or router_hostname must '
     47                               'be specified to build_router_hostname.')
     48 
     49     return dnsname_mangler.get_router_addr(client_hostname,
     50                                            cmdline_override=router_hostname)
     51 
     52 
     53 def build_router_proxy(test_name='', client_hostname=None, router_addr=None,
     54                        enable_avahi=False):
     55     """Build up a LinuxRouter object.
     56 
     57     Verifies that the remote host responds to ping.
     58     Either client_hostname or router_addr must be specified.
     59 
     60     @param test_name: string name of this test (e.g. 'network_WiFi_TestName').
     61     @param client_hostname: string hostname of DUT if we're in the lab.
     62     @param router_addr: string DNS/IPv4 address to use for router host object.
     63     @param enable_avahi: boolean True iff avahi should be started on the router.
     64 
     65     @return LinuxRouter or raise error.TestError on failure.
     66 
     67     """
     68     router_hostname = build_router_hostname(client_hostname=client_hostname,
     69                                             router_hostname=router_addr)
     70     logging.info('Connecting to router at %s', router_hostname)
     71     ping_helper = ping_runner.PingRunner()
     72     if not ping_helper.simple_ping(router_hostname):
     73         raise error.TestError('Router at %s is not pingable.' %
     74                               router_hostname)
     75 
     76     return LinuxRouter(hosts.create_host(router_hostname), test_name,
     77                        enable_avahi=enable_avahi)
     78 
     79 
     80 class LinuxRouter(site_linux_system.LinuxSystem):
     81     """Linux/mac80211-style WiFi Router support for WiFiTest class.
     82 
     83     This class implements test methods/steps that communicate with a
     84     router implemented with Linux/mac80211.  The router must
     85     be pre-configured to enable ssh access and have a mac80211-based
     86     wireless device.  We also assume hostapd 0.7.x and iw are present
     87     and any necessary modules are pre-loaded.
     88 
     89     """
     90 
     91     KNOWN_TEST_PREFIX = 'network_WiFi_'
     92     POLLING_INTERVAL_SECONDS = 0.5
     93     STARTUP_TIMEOUT_SECONDS = 10
     94     SUFFIX_LETTERS = string.ascii_lowercase + string.digits
     95     SUBNET_PREFIX_OCTETS = (192, 168)
     96 
     97     HOSTAPD_CONF_FILE_PATTERN = '/tmp/hostapd-test-%s.conf'
     98     HOSTAPD_LOG_FILE_PATTERN = '/tmp/hostapd-test-%s.log'
     99     HOSTAPD_STDERR_LOG_FILE_PATTERN = '/tmp/hostapd-stderr-test-%s.log'
    100     HOSTAPD_CONTROL_INTERFACE_PATTERN = '/tmp/hostapd-test-%s.ctrl'
    101     HOSTAPD_DRIVER_NAME = 'nl80211'
    102 
    103     STATION_CONF_FILE_PATTERN = '/tmp/wpa-supplicant-test-%s.conf'
    104     STATION_LOG_FILE_PATTERN = '/tmp/wpa-supplicant-test-%s.log'
    105     STATION_PID_FILE_PATTERN = '/tmp/wpa-supplicant-test-%s.pid'
    106 
    107     MGMT_FRAME_SENDER_LOG_FILE = '/tmp/send_management_frame-test.log'
    108 
    109     PROBE_RESPONSE_FOOTER_FILE = '/tmp/autotest-probe_response_footer'
    110 
    111     def get_capabilities(self):
    112         """@return iterable object of AP capabilities for this system."""
    113         caps = set()
    114         try:
    115             self.cmd_send_management_frame = path_utils.must_be_installed(
    116                     '/usr/bin/send_management_frame', host=self.host)
    117             caps.add(self.CAPABILITY_SEND_MANAGEMENT_FRAME)
    118         except error.TestFail:
    119             pass
    120         return super(LinuxRouter, self).get_capabilities().union(caps)
    121 
    122 
    123     @property
    124     def router(self):
    125         """Deprecated.  Use self.host instead.
    126 
    127         @return Host object representing the remote router.
    128 
    129         """
    130         return self.host
    131 
    132 
    133     @property
    134     def wifi_ip(self):
    135         """Simple accessor for the WiFi IP when there is only one AP.
    136 
    137         @return string IP of WiFi interface.
    138 
    139         """
    140         if len(self.local_servers) != 1:
    141             raise error.TestError('Could not pick a WiFi IP to return.')
    142 
    143         return self.get_wifi_ip(0)
    144 
    145 
    146     def __init__(self, host, test_name, enable_avahi=False):
    147         """Build a LinuxRouter.
    148 
    149         @param host Host object representing the remote machine.
    150         @param test_name string name of this test.  Used in SSID creation.
    151         @param enable_avahi: boolean True iff avahi should be started on the
    152                 router.
    153 
    154         """
    155         super(LinuxRouter, self).__init__(host, 'router')
    156         self._ssid_prefix = test_name
    157         self._enable_avahi = enable_avahi
    158         self.__setup()
    159 
    160 
    161     def __setup(self):
    162         """Set up this system.
    163 
    164         Can be used either to complete initialization of a LinuxRouter
    165         object, or to re-establish a good state after a reboot.
    166 
    167         """
    168         self.cmd_dhcpd = '/usr/sbin/dhcpd'
    169         self.cmd_hostapd = path_utils.must_be_installed(
    170                 '/usr/sbin/hostapd', host=self.host)
    171         self.cmd_hostapd_cli = path_utils.must_be_installed(
    172                 '/usr/sbin/hostapd_cli', host=self.host)
    173         self.cmd_wpa_supplicant = path_utils.must_be_installed(
    174                 '/usr/sbin/wpa_supplicant', host=self.host)
    175         self.dhcpd_conf = '/tmp/dhcpd.%s.conf'
    176         self.dhcpd_leases = '/tmp/dhcpd.leases'
    177 
    178         # Log the most recent message on the router so that we can rebuild the
    179         # suffix relevant to us when debugging failures.
    180         last_log_line = self.host.run('tail -1 /var/log/messages').stdout
    181         # We're trying to get the timestamp from:
    182         # 2014-07-23T17:29:34.961056+00:00 localhost kernel: blah blah blah
    183         self._log_start_timestamp = last_log_line.strip().split(None, 2)[0]
    184         logging.debug('Will only retrieve logs after %s.',
    185                       self._log_start_timestamp)
    186 
    187         # hostapd configuration persists throughout the test, subsequent
    188         # 'config' commands only modify it.
    189         if self._ssid_prefix.startswith(self.KNOWN_TEST_PREFIX):
    190             # Many of our tests start with an uninteresting prefix.
    191             # Remove it so we can have more unique bytes.
    192             self._ssid_prefix = self._ssid_prefix[len(self.KNOWN_TEST_PREFIX):]
    193         self._number_unique_ssids = 0
    194 
    195         self._total_hostapd_instances = 0
    196         self.local_servers = []
    197         self.server_address_index = []
    198         self.hostapd_instances = []
    199         self.station_instances = []
    200         self.dhcp_low = 1
    201         self.dhcp_high = 128
    202 
    203         # Kill hostapd and dhcp server if already running.
    204         self._kill_process_instance('hostapd', timeout_seconds=30)
    205         self.stop_dhcp_server(instance=None)
    206 
    207         # Place us in the US by default
    208         self.iw_runner.set_regulatory_domain('US')
    209 
    210         self.enable_all_antennas()
    211 
    212         # Some tests want this functionality, but otherwise, it's a distraction.
    213         if self._enable_avahi:
    214             self.host.run('start avahi', ignore_status=True)
    215         else:
    216             self.host.run('stop avahi', ignore_status=True)
    217 
    218 
    219     def close(self):
    220         """Close global resources held by this system."""
    221         self.deconfig()
    222         # dnsmasq and hostapd cause interesting events to go to system logs.
    223         # Retrieve only the suffix of the logs after the timestamp we stored on
    224         # router creation.
    225         self.host.run("sed -n -e '/%s/,$p' /var/log/messages >/tmp/router_log" %
    226                       self._log_start_timestamp, ignore_status=True)
    227         self.host.get_file('/tmp/router_log', 'debug/router_host_messages')
    228         super(LinuxRouter, self).close()
    229 
    230 
    231     def reboot(self, timeout):
    232         """Reboot this router, and restore it to a known-good state.
    233 
    234         @param timeout Maximum seconds to wait for router to return.
    235 
    236         """
    237         super(LinuxRouter, self).reboot(timeout)
    238         self.__setup()
    239 
    240 
    241     def has_local_server(self):
    242         """@return True iff this router has local servers configured."""
    243         return bool(self.local_servers)
    244 
    245 
    246     def start_hostapd(self, configuration):
    247         """Start a hostapd instance described by conf.
    248 
    249         @param configuration HostapConfig object.
    250 
    251         """
    252         # Figure out the correct interface.
    253         if configuration.min_streams is None:
    254             interface = self.get_wlanif(configuration.frequency, 'managed')
    255         else:
    256             interface = self.get_wlanif(
    257                 configuration.frequency, 'managed', configuration.min_streams)
    258 
    259         conf_file = self.HOSTAPD_CONF_FILE_PATTERN % interface
    260         log_file = self.HOSTAPD_LOG_FILE_PATTERN % interface
    261         stderr_log_file = self.HOSTAPD_STDERR_LOG_FILE_PATTERN % interface
    262         control_interface = self.HOSTAPD_CONTROL_INTERFACE_PATTERN % interface
    263         hostapd_conf_dict = configuration.generate_dict(
    264                 interface, control_interface,
    265                 self.build_unique_ssid(suffix=configuration.ssid_suffix))
    266         logging.debug('hostapd parameters: %r', hostapd_conf_dict)
    267 
    268         # Generate hostapd.conf.
    269         self.router.run("cat <<EOF >%s\n%s\nEOF\n" %
    270             (conf_file, '\n'.join(
    271             "%s=%s" % kv for kv in hostapd_conf_dict.iteritems())))
    272 
    273         # Run hostapd.
    274         logging.info('Starting hostapd on %s(%s) channel=%s...',
    275                      interface, self.iw_runner.get_interface(interface).phy,
    276                      configuration.channel)
    277         self.router.run('rm %s' % log_file, ignore_status=True)
    278         self.router.run('stop wpasupplicant', ignore_status=True)
    279         start_command = '%s -dd -t %s > %s 2> %s & echo $!' % (
    280                 self.cmd_hostapd, conf_file, log_file, stderr_log_file)
    281         pid = int(self.router.run(start_command).stdout.strip())
    282         self.hostapd_instances.append(HostapdInstance(
    283                 hostapd_conf_dict['ssid'],
    284                 conf_file,
    285                 log_file,
    286                 interface,
    287                 hostapd_conf_dict.copy(),
    288                 stderr_log_file,
    289                 configuration.scenario_name))
    290 
    291         # Wait for confirmation that the router came up.
    292         logging.info('Waiting for hostapd to startup.')
    293         start_time = time.time()
    294         while time.time() - start_time < self.STARTUP_TIMEOUT_SECONDS:
    295             success = self.router.run(
    296                     'grep "Setup of interface done" %s' % log_file,
    297                     ignore_status=True).exit_status == 0
    298             if success:
    299                 break
    300 
    301             # A common failure is an invalid router configuration.
    302             # Detect this and exit early if we see it.
    303             bad_config = self.router.run(
    304                     'grep "Interface initialization failed" %s' % log_file,
    305                     ignore_status=True).exit_status == 0
    306             if bad_config:
    307                 raise error.TestFail('hostapd failed to initialize AP '
    308                                      'interface.')
    309 
    310             if pid:
    311                 early_exit = self.router.run('kill -0 %d' % pid,
    312                                              ignore_status=True).exit_status
    313                 if early_exit:
    314                     raise error.TestFail('hostapd process terminated.')
    315 
    316             time.sleep(self.POLLING_INTERVAL_SECONDS)
    317         else:
    318             raise error.TestFail('Timed out while waiting for hostapd '
    319                                  'to start.')
    320 
    321 
    322     def _kill_process_instance(self,
    323                                process,
    324                                instance=None,
    325                                timeout_seconds=10,
    326                                ignore_timeouts=False):
    327         """Kill a process on the router.
    328 
    329         Kills remote program named |process| (optionally only a specific
    330         |instance|).  Wait |timeout_seconds| for |process| to die
    331         before returning.  If |ignore_timeouts| is False, raise
    332         a TestError on timeouts.
    333 
    334         @param process: string name of process to kill.
    335         @param instance: string fragment of the command line unique to
    336                 this instance of the remote process.
    337         @param timeout_seconds: float timeout in seconds to wait.
    338         @param ignore_timeouts: True iff we should ignore failures to
    339                 kill processes.
    340         @return True iff the specified process has exited.
    341 
    342         """
    343         if instance is not None:
    344             search_arg = '-f "^%s.*%s"' % (process, instance)
    345         else:
    346             search_arg = process
    347 
    348         self.host.run('pkill %s' % search_arg, ignore_status=True)
    349         is_dead = False
    350         start_time = time.time()
    351         while not is_dead and time.time() - start_time < timeout_seconds:
    352             time.sleep(self.POLLING_INTERVAL_SECONDS)
    353             is_dead = self.host.run(
    354                     'pgrep -l %s' % search_arg,
    355                     ignore_status=True).exit_status != 0
    356         if is_dead or ignore_timeouts:
    357             return is_dead
    358 
    359         raise error.TestError(
    360                 'Timed out waiting for %s%s to die' %
    361                 (process,
    362                 '' if instance is None else ' (instance=%s)' % instance))
    363 
    364 
    365     def kill_hostapd_instance(self, instance):
    366         """Kills a hostapd instance.
    367 
    368         @param instance HostapdInstance object.
    369 
    370         """
    371         is_dead = self._kill_process_instance(
    372                 self.cmd_hostapd,
    373                 instance=instance.conf_file,
    374                 timeout_seconds=30,
    375                 ignore_timeouts=True)
    376         if instance.scenario_name:
    377             log_identifier = instance.scenario_name
    378         else:
    379             log_identifier = '%d_%s' % (
    380                 self._total_hostapd_instances, instance.interface)
    381         files_to_copy = [(instance.log_file,
    382                           'debug/hostapd_router_%s.log' % log_identifier),
    383                          (instance.stderr_log_file,
    384                           'debug/hostapd_router_%s.stderr.log' %
    385                           log_identifier)]
    386         for remote_file, local_file in files_to_copy:
    387             if self.host.run('ls %s >/dev/null 2>&1' % remote_file,
    388                              ignore_status=True).exit_status:
    389                 logging.error('Did not collect hostapd log file because '
    390                               'it was missing.')
    391             else:
    392                 self.router.get_file(remote_file, local_file)
    393         self._total_hostapd_instances += 1
    394         if not is_dead:
    395             raise error.TestError('Timed out killing hostapd.')
    396 
    397 
    398     def build_unique_ssid(self, suffix=''):
    399         """ Build our unique token by base-<len(self.SUFFIX_LETTERS)> encoding
    400         the number of APs we've constructed already.
    401 
    402         @param suffix string to append to SSID
    403 
    404         """
    405         base = len(self.SUFFIX_LETTERS)
    406         number = self._number_unique_ssids
    407         self._number_unique_ssids += 1
    408         unique = ''
    409         while number or not unique:
    410             unique = self.SUFFIX_LETTERS[number % base] + unique
    411             number = number / base
    412         # And salt the SSID so that tests running in adjacent cells are unlikely
    413         # to pick the same SSID and we're resistent to beacons leaking out of
    414         # cells.
    415         salt = ''.join([random.choice(self.SUFFIX_LETTERS) for x in range(5)])
    416         return '_'.join([self._ssid_prefix, unique, salt, suffix])[-32:]
    417 
    418 
    419     def hostap_configure(self, configuration, multi_interface=None):
    420         """Build up a hostapd configuration file and start hostapd.
    421 
    422         Also setup a local server if this router supports them.
    423 
    424         @param configuration HosetapConfig object.
    425         @param multi_interface bool True iff multiple interfaces allowed.
    426 
    427         """
    428         if multi_interface is None and (self.hostapd_instances or
    429                                         self.station_instances):
    430             self.deconfig()
    431         if configuration.is_11ac:
    432             router_caps = self.get_capabilities()
    433             if site_linux_system.LinuxSystem.CAPABILITY_VHT not in router_caps:
    434                 raise error.TestNAError('Router does not have AC support')
    435 
    436         self.start_hostapd(configuration)
    437         interface = self.hostapd_instances[-1].interface
    438         self.iw_runner.set_tx_power(interface, 'auto')
    439         self.set_beacon_footer(interface, configuration.beacon_footer)
    440         self.start_local_server(interface)
    441         logging.info('AP configured.')
    442 
    443 
    444     def ibss_configure(self, config):
    445         """Configure a station based AP in IBSS mode.
    446 
    447         Extract relevant configuration objects from |config| despite not
    448         actually being a hostap managed endpoint.
    449 
    450         @param config HostapConfig object.
    451 
    452         """
    453         if self.station_instances or self.hostapd_instances:
    454             self.deconfig()
    455         interface = self.get_wlanif(config.frequency, 'ibss')
    456         ssid = (config.ssid or
    457                 self.build_unique_ssid(suffix=config.ssid_suffix))
    458         # Connect the station
    459         self.router.run('%s link set %s up' % (self.cmd_ip, interface))
    460         self.iw_runner.ibss_join(interface, ssid, config.frequency)
    461         # Always start a local server.
    462         self.start_local_server(interface)
    463         # Remember that this interface is up.
    464         self.station_instances.append(
    465                 StationInstance(ssid=ssid, interface=interface,
    466                                 dev_type='ibss'))
    467 
    468 
    469     def local_server_address(self, index):
    470         """Get the local server address for an interface.
    471 
    472         When we multiple local servers, we give them static IP addresses
    473         like 192.168.*.254.
    474 
    475         @param index int describing which local server this is for.
    476 
    477         """
    478         return '%d.%d.%d.%d' % (self.SUBNET_PREFIX_OCTETS + (index, 254))
    479 
    480 
    481     def local_peer_ip_address(self, index):
    482         """Get the IP address allocated for the peer associated to the AP.
    483 
    484         This address is assigned to a locally associated peer device that
    485         is created for the DUT to perform connectivity tests with.
    486         When we have multiple local servers, we give them static IP addresses
    487         like 192.168.*.253.
    488 
    489         @param index int describing which local server this is for.
    490 
    491         """
    492         return '%d.%d.%d.%d' % (self.SUBNET_PREFIX_OCTETS + (index, 253))
    493 
    494 
    495     def local_peer_mac_address(self):
    496         """Get the MAC address of the peer interface.
    497 
    498         @return string MAC address of the peer interface.
    499 
    500         """
    501         iface = interface.Interface(self.station_instances[0].interface,
    502                                     self.router)
    503         return iface.mac_address
    504 
    505 
    506     def _get_unused_server_address_index(self):
    507         """@return an unused server address index."""
    508         for address_index in range(0, 256):
    509             if address_index not in self.server_address_index:
    510                 return address_index
    511         raise error.TestFail('No available server address index')
    512 
    513 
    514     def change_server_address_index(self, ap_num=0, server_address_index=None):
    515         """Restart the local server with a different server address index.
    516 
    517         This will restart the local server with different gateway IP address
    518         and DHCP address ranges.
    519 
    520         @param ap_num: int hostapd instance number.
    521         @param server_address_index: int server address index.
    522 
    523         """
    524         interface = self.local_servers[ap_num]['interface'];
    525         # Get an unused server address index if one is not specified, which
    526         # will be different from the one that's currently in used.
    527         if server_address_index is None:
    528             server_address_index = self._get_unused_server_address_index()
    529 
    530         # Restart local server with the new server address index.
    531         self.stop_local_server(self.local_servers[ap_num])
    532         self.start_local_server(interface,
    533                                 ap_num=ap_num,
    534                                 server_address_index=server_address_index)
    535 
    536 
    537     def start_local_server(self,
    538                            interface,
    539                            ap_num=None,
    540                            server_address_index=None):
    541         """Start a local server on an interface.
    542 
    543         @param interface string (e.g. wlan0)
    544         @param ap_num int the ap instance to start the server for
    545         @param server_address_index int server address index
    546 
    547         """
    548         logging.info('Starting up local server...')
    549 
    550         if len(self.local_servers) >= 256:
    551             raise error.TestFail('Exhausted available local servers')
    552 
    553         # Get an unused server address index if one is not specified.
    554         # Validate server address index if one is specified.
    555         if server_address_index is None:
    556             server_address_index = self._get_unused_server_address_index()
    557         elif server_address_index in self.server_address_index:
    558             raise error.TestFail('Server address index %d already in used' %
    559                                  server_address_index)
    560 
    561         server_addr = netblock.from_addr(
    562                 self.local_server_address(server_address_index),
    563                 prefix_len=24)
    564 
    565         params = {}
    566         params['address_index'] = server_address_index
    567         params['netblock'] = server_addr
    568         params['dhcp_range'] = ' '.join(
    569             (server_addr.get_addr_in_block(1),
    570              server_addr.get_addr_in_block(128)))
    571         params['interface'] = interface
    572         params['ip_params'] = ('%s broadcast %s dev %s' %
    573                                (server_addr.netblock,
    574                                 server_addr.broadcast,
    575                                 interface))
    576         if ap_num is None:
    577             self.local_servers.append(params)
    578         else:
    579             self.local_servers.insert(ap_num, params)
    580         self.server_address_index.append(server_address_index)
    581 
    582         self.router.run('%s addr flush %s' %
    583                         (self.cmd_ip, interface))
    584         self.router.run('%s addr add %s' %
    585                         (self.cmd_ip, params['ip_params']))
    586         self.router.run('%s link set %s up' %
    587                         (self.cmd_ip, interface))
    588         self.start_dhcp_server(interface)
    589 
    590 
    591     def stop_local_server(self, server):
    592         """Stop a local server on the router
    593 
    594         @param server object server configuration parameters.
    595 
    596         """
    597         self.stop_dhcp_server(server['interface'])
    598         self.router.run("%s addr del %s" %
    599                         (self.cmd_ip, server['ip_params']),
    600                         ignore_status=True)
    601         self.server_address_index.remove(server['address_index'])
    602         self.local_servers.remove(server)
    603 
    604 
    605     def start_dhcp_server(self, interface):
    606         """Start a dhcp server on an interface.
    607 
    608         @param interface string (e.g. wlan0)
    609 
    610         """
    611         for server in self.local_servers:
    612             if server['interface'] == interface:
    613                 params = server
    614                 break
    615         else:
    616             raise error.TestFail('Could not find local server '
    617                                  'to match interface: %r' % interface)
    618         server_addr = params['netblock']
    619         dhcpd_conf_file = self.dhcpd_conf % interface
    620         dhcp_conf = '\n'.join([
    621             'port=0',  # disables DNS server
    622             'bind-interfaces',
    623             'log-dhcp',
    624             'dhcp-range=%s' % ','.join((server_addr.get_addr_in_block(1),
    625                                         server_addr.get_addr_in_block(128))),
    626             'interface=%s' % params['interface'],
    627             'dhcp-leasefile=%s' % self.dhcpd_leases])
    628         self.router.run('cat <<EOF >%s\n%s\nEOF\n' %
    629             (dhcpd_conf_file, dhcp_conf))
    630         self.router.run('dnsmasq --conf-file=%s' % dhcpd_conf_file)
    631 
    632 
    633     def stop_dhcp_server(self, instance=None):
    634         """Stop a dhcp server on the router.
    635 
    636         @param instance string instance to kill.
    637 
    638         """
    639         self._kill_process_instance('dnsmasq', instance=instance)
    640 
    641 
    642     def get_wifi_channel(self, ap_num):
    643         """Return channel of BSS corresponding to |ap_num|.
    644 
    645         @param ap_num int which BSS to get the channel of.
    646         @return int primary channel of BSS.
    647 
    648         """
    649         instance = self.hostapd_instances[ap_num]
    650         return instance.config_dict['channel']
    651 
    652 
    653     def get_wifi_ip(self, ap_num):
    654         """Return IP address on the WiFi subnet of a local server on the router.
    655 
    656         If no local servers are configured (e.g. for an RSPro), a TestFail will
    657         be raised.
    658 
    659         @param ap_num int which local server to get an address from.
    660 
    661         """
    662         if not self.local_servers:
    663             raise error.TestError('No IP address assigned')
    664 
    665         return self.local_servers[ap_num]['netblock'].addr
    666 
    667 
    668     def get_wifi_ip_subnet(self, ap_num):
    669         """Return subnet of WiFi AP instance.
    670 
    671         If no APs are configured a TestError will be raised.
    672 
    673         @param ap_num int which local server to get an address from.
    674 
    675         """
    676         if not self.local_servers:
    677             raise error.TestError('No APs configured.')
    678 
    679         return self.local_servers[ap_num]['netblock'].subnet
    680 
    681 
    682     def get_hostapd_interface(self, ap_num):
    683         """Get the name of the interface associated with a hostapd instance.
    684 
    685         @param ap_num: int hostapd instance number.
    686         @return string interface name (e.g. 'managed0').
    687 
    688         """
    689         if ap_num not in range(len(self.hostapd_instances)):
    690             raise error.TestFail('Invalid instance number (%d) with %d '
    691                                  'instances configured.' %
    692                                  (ap_num, len(self.hostapd_instances)))
    693 
    694         instance = self.hostapd_instances[ap_num]
    695         return instance.interface
    696 
    697 
    698     def get_station_interface(self, instance):
    699         """Get the name of the interface associated with a station.
    700 
    701         @param instance: int station instance number.
    702         @return string interface name (e.g. 'managed0').
    703 
    704         """
    705         if instance not in range(len(self.station_instances)):
    706             raise error.TestFail('Invalid instance number (%d) with %d '
    707                                  'instances configured.' %
    708                                  (instance, len(self.station_instances)))
    709 
    710         instance = self.station_instances[instance]
    711         return instance.interface
    712 
    713 
    714     def get_hostapd_mac(self, ap_num):
    715         """Return the MAC address of an AP in the test.
    716 
    717         @param ap_num int index of local server to read the MAC address from.
    718         @return string MAC address like 00:11:22:33:44:55.
    719 
    720         """
    721         interface_name = self.get_hostapd_interface(ap_num)
    722         ap_interface = interface.Interface(interface_name, self.host)
    723         return ap_interface.mac_address
    724 
    725 
    726     def get_hostapd_phy(self, ap_num):
    727         """Get name of phy for hostapd instance.
    728 
    729         @param ap_num int index of hostapd instance.
    730         @return string phy name of phy corresponding to hostapd's
    731                 managed interface.
    732 
    733         """
    734         interface = self.iw_runner.get_interface(
    735                 self.get_hostapd_interface(ap_num))
    736         return interface.phy
    737 
    738 
    739     def deconfig(self):
    740         """A legacy, deprecated alias for deconfig_aps."""
    741         self.deconfig_aps()
    742 
    743 
    744     def deconfig_aps(self, instance=None, silent=False):
    745         """De-configure an AP (will also bring wlan down).
    746 
    747         @param instance: int or None.  If instance is None, will bring down all
    748                 instances of hostapd.
    749         @param silent: True if instances should be brought without de-authing
    750                 the DUT.
    751 
    752         """
    753         if not self.hostapd_instances and not self.station_instances:
    754             return
    755 
    756         if self.hostapd_instances:
    757             local_servers = []
    758             if instance is not None:
    759                 instances = [ self.hostapd_instances.pop(instance) ]
    760                 for server in self.local_servers:
    761                     if server['interface'] == instances[0].interface:
    762                         local_servers = [server]
    763                         break
    764             else:
    765                 instances = self.hostapd_instances
    766                 self.hostapd_instances = []
    767                 local_servers = copy.copy(self.local_servers)
    768 
    769             for instance in instances:
    770                 if silent:
    771                     # Deconfigure without notifying DUT.  Remove the interface
    772                     # hostapd uses to send beacon and DEAUTH packets.
    773                     self.remove_interface(instance.interface)
    774 
    775                 self.kill_hostapd_instance(instance)
    776                 self.release_interface(instance.interface)
    777         if self.station_instances:
    778             local_servers = copy.copy(self.local_servers)
    779             instance = self.station_instances.pop()
    780             if instance.dev_type == 'ibss':
    781                 self.iw_runner.ibss_leave(instance.interface)
    782             elif instance.dev_type == 'managed':
    783                 self._kill_process_instance(self.cmd_wpa_supplicant,
    784                                             instance=instance.interface)
    785             else:
    786                 self.iw_runner.disconnect_station(instance.interface)
    787             self.router.run('%s link set %s down' %
    788                             (self.cmd_ip, instance.interface))
    789 
    790         for server in local_servers:
    791             self.stop_local_server(server)
    792 
    793 
    794     def set_ap_interface_down(self, instance=0):
    795         """Bring down the hostapd interface.
    796 
    797         @param instance int router instance number.
    798 
    799         """
    800         self.host.run('%s link set %s down' %
    801                       (self.cmd_ip, self.get_hostapd_interface(instance)))
    802 
    803 
    804     def confirm_pmksa_cache_use(self, instance=0):
    805         """Verify that the PMKSA auth was cached on a hostapd instance.
    806 
    807         @param instance int router instance number.
    808 
    809         """
    810         log_file = self.hostapd_instances[instance].log_file
    811         pmksa_match = 'PMK from PMKSA cache'
    812         result = self.router.run('grep -q "%s" %s' % (pmksa_match, log_file),
    813                                  ignore_status=True)
    814         if result.exit_status:
    815             raise error.TestFail('PMKSA cache was not used in roaming.')
    816 
    817 
    818     def get_ssid(self, instance=None):
    819         """@return string ssid for the network stemming from this router."""
    820         if instance is None:
    821             instance = 0
    822             if len(self.hostapd_instances) > 1:
    823                 raise error.TestFail('No instance of hostapd specified with '
    824                                      'multiple instances present.')
    825 
    826         if self.hostapd_instances:
    827             return self.hostapd_instances[instance].ssid
    828 
    829         if self.station_instances:
    830             return self.station_instances[0].ssid
    831 
    832         raise error.TestFail('Requested ssid of an unconfigured AP.')
    833 
    834 
    835     def deauth_client(self, client_mac):
    836         """Deauthenticates a client described in params.
    837 
    838         @param client_mac string containing the mac address of the client to be
    839                deauthenticated.
    840 
    841         """
    842         control_if = self.hostapd_instances[-1].config_dict['ctrl_interface']
    843         self.router.run('%s -p%s deauthenticate %s' %
    844                         (self.cmd_hostapd_cli, control_if, client_mac))
    845 
    846 
    847     def _prep_probe_response_footer(self, footer):
    848         """Write probe response footer temporarily to a local file and copy
    849         over to test router.
    850 
    851         @param footer string containing bytes for the probe response footer.
    852         @raises AutoservRunError: If footer file copy fails.
    853 
    854         """
    855         with tempfile.NamedTemporaryFile() as fp:
    856             fp.write(footer)
    857             fp.flush()
    858             try:
    859                 self.host.send_file(fp.name, self.PROBE_RESPONSE_FOOTER_FILE)
    860             except error.AutoservRunError:
    861                 logging.error('failed to copy footer file to AP')
    862                 raise
    863 
    864 
    865     def send_management_frame_on_ap(self, frame_type, channel, instance=0):
    866         """Injects a management frame into an active hostapd session.
    867 
    868         @param frame_type string the type of frame to send.
    869         @param channel int targeted channel
    870         @param instance int indicating which hostapd instance to inject into.
    871 
    872         """
    873         hostap_interface = self.hostapd_instances[instance].interface
    874         interface = self.get_wlanif(0, 'monitor', same_phy_as=hostap_interface)
    875         self.router.run("%s link set %s up" % (self.cmd_ip, interface))
    876         self.router.run('%s -i %s -t %s -c %d' %
    877                         (self.cmd_send_management_frame, interface, frame_type,
    878                          channel))
    879         self.release_interface(interface)
    880 
    881 
    882     def setup_management_frame_interface(self, channel):
    883         """
    884         Setup interface for injecting management frames.
    885 
    886         @param channel int channel to inject the frames.
    887 
    888         @return string name of the interface.
    889 
    890         """
    891         frequency = hostap_config.HostapConfig.get_frequency_for_channel(
    892                 channel)
    893         interface = self.get_wlanif(frequency, 'monitor')
    894         self.router.run('%s link set %s up' % (self.cmd_ip, interface))
    895         self.iw_runner.set_freq(interface, frequency)
    896         return interface
    897 
    898 
    899     def send_management_frame(self, interface, frame_type, channel,
    900                               ssid_prefix=None, num_bss=None,
    901                               frame_count=None, delay=None,
    902                               dest_addr=None, probe_resp_footer=None):
    903         """
    904         Injects management frames on specify channel |frequency|.
    905 
    906         This function will spawn off a new process to inject specified
    907         management frames |frame_type| at the specified interface |interface|.
    908 
    909         @param interface string interface to inject frames.
    910         @param frame_type string message type.
    911         @param channel int targeted channel.
    912         @param ssid_prefix string SSID prefix.
    913         @param num_bss int number of BSS.
    914         @param frame_count int number of frames to send.
    915         @param delay int milliseconds delay between frames.
    916         @param dest_addr string destination address (DA) MAC address.
    917         @param probe_resp_footer string footer for probe response.
    918 
    919         @return int PID of the newly created process.
    920 
    921         """
    922         command = '%s -i %s -t %s -c %d' % (self.cmd_send_management_frame,
    923                                 interface, frame_type, channel)
    924         if ssid_prefix is not None:
    925             command += ' -s %s' % (ssid_prefix)
    926         if num_bss is not None:
    927             command += ' -b %d' % (num_bss)
    928         if frame_count is not None:
    929             command += ' -n %d' % (frame_count)
    930         if delay is not None:
    931             command += ' -d %d' % (delay)
    932         if dest_addr is not None:
    933             command += ' -a %s' % (dest_addr)
    934         if probe_resp_footer is not None:
    935             self._prep_probe_response_footer(footer=probe_resp_footer)
    936             command += ' -f %s' % (self.PROBE_RESPONSE_FOOTER_FILE)
    937         command += ' > %s 2>&1 & echo $!' % (self.MGMT_FRAME_SENDER_LOG_FILE)
    938         pid = int(self.router.run(command).stdout)
    939         return pid
    940 
    941 
    942     def detect_client_deauth(self, client_mac, instance=0):
    943         """Detects whether hostapd has logged a deauthentication from
    944         |client_mac|.
    945 
    946         @param client_mac string the MAC address of the client to detect.
    947         @param instance int indicating which hostapd instance to query.
    948 
    949         """
    950         interface = self.hostapd_instances[instance].interface
    951         deauth_msg = "%s: deauthentication: STA=%s" % (interface, client_mac)
    952         log_file = self.hostapd_instances[instance].log_file
    953         result = self.router.run("grep -qi '%s' %s" % (deauth_msg, log_file),
    954                                  ignore_status=True)
    955         return result.exit_status == 0
    956 
    957 
    958     def detect_client_coexistence_report(self, client_mac, instance=0):
    959         """Detects whether hostapd has logged an action frame from
    960         |client_mac| indicating information about 20/40MHz BSS coexistence.
    961 
    962         @param client_mac string the MAC address of the client to detect.
    963         @param instance int indicating which hostapd instance to query.
    964 
    965         """
    966         coex_msg = ('nl80211: MLME event frame - hexdump(len=.*): '
    967                     '.. .. .. .. .. .. .. .. .. .. %s '
    968                     '.. .. .. .. .. .. .. .. 04 00.*48 01 ..' %
    969                     ' '.join(client_mac.split(':')))
    970         log_file = self.hostapd_instances[instance].log_file
    971         result = self.router.run("grep -qi '%s' %s" % (coex_msg, log_file),
    972                                  ignore_status=True)
    973         return result.exit_status == 0
    974 
    975 
    976     def add_connected_peer(self, instance=0):
    977         """Configure a station connected to a running AP instance.
    978 
    979         Extract relevant configuration objects from the hostap
    980         configuration for |instance| and generate a wpa_supplicant
    981         instance that connects to it.  This allows the DUT to interact
    982         with a client entity that is also connected to the same AP.  A
    983         full wpa_supplicant instance is necessary here (instead of just
    984         using the "iw" command to connect) since we want to enable
    985         advanced features such as TDLS.
    986 
    987         @param instance int indicating which hostapd instance to connect to.
    988 
    989         """
    990         if not self.hostapd_instances:
    991             raise error.TestFail('Hostapd is not configured.')
    992 
    993         if self.station_instances:
    994             raise error.TestFail('Station is already configured.')
    995 
    996         ssid = self.get_ssid(instance)
    997         hostap_conf = self.hostapd_instances[instance].config_dict
    998         frequency = hostap_config.HostapConfig.get_frequency_for_channel(
    999                 hostap_conf['channel'])
   1000         self.configure_managed_station(
   1001                 ssid, frequency, self.local_peer_ip_address(instance))
   1002         interface = self.station_instances[0].interface
   1003         # Since we now have two network interfaces connected to the same
   1004         # network, we need to disable the kernel's protection against
   1005         # incoming packets to an "unexpected" interface.
   1006         self.router.run('echo 2 > /proc/sys/net/ipv4/conf/%s/rp_filter' %
   1007                         interface)
   1008 
   1009         # Similarly, we'd like to prevent the hostap interface from
   1010         # replying to ARP requests for the peer IP address and vice
   1011         # versa.
   1012         self.router.run('echo 1 > /proc/sys/net/ipv4/conf/%s/arp_ignore' %
   1013                         interface)
   1014         self.router.run('echo 1 > /proc/sys/net/ipv4/conf/%s/arp_ignore' %
   1015                         hostap_conf['interface'])
   1016 
   1017 
   1018     def configure_managed_station(self, ssid, frequency, ip_addr):
   1019         """Configure a router interface to connect as a client to a network.
   1020 
   1021         @param ssid: string SSID of network to join.
   1022         @param frequency: int frequency required to join the network.
   1023         @param ip_addr: IP address to assign to this interface
   1024                         (e.g. '192.168.1.200').
   1025 
   1026         """
   1027         interface = self.get_wlanif(frequency, 'managed')
   1028 
   1029         # TODO(pstew): Configure other bits like PSK, 802.11n if tests
   1030         # require them...
   1031         supplicant_config = (
   1032                 'network={\n'
   1033                 '  ssid="%(ssid)s"\n'
   1034                 '  key_mgmt=NONE\n'
   1035                 '}\n' % {'ssid': ssid}
   1036         )
   1037 
   1038         conf_file = self.STATION_CONF_FILE_PATTERN % interface
   1039         log_file = self.STATION_LOG_FILE_PATTERN % interface
   1040         pid_file = self.STATION_PID_FILE_PATTERN % interface
   1041 
   1042         self.router.run('cat <<EOF >%s\n%s\nEOF\n' %
   1043             (conf_file, supplicant_config))
   1044 
   1045         # Connect the station.
   1046         self.router.run('%s link set %s up' % (self.cmd_ip, interface))
   1047         start_command = ('%s -dd -t -i%s -P%s -c%s -D%s >%s 2>&1 &' %
   1048                          (self.cmd_wpa_supplicant,
   1049                          interface, pid_file, conf_file,
   1050                          self.HOSTAPD_DRIVER_NAME, log_file))
   1051         self.router.run(start_command)
   1052         self.iw_runner.wait_for_link(interface)
   1053 
   1054         # Assign an IP address to this interface.
   1055         self.router.run('%s addr add %s/24 dev %s' %
   1056                         (self.cmd_ip, ip_addr, interface))
   1057         self.station_instances.append(
   1058                 StationInstance(ssid=ssid, interface=interface,
   1059                                 dev_type='managed'))
   1060 
   1061 
   1062     def send_magic_packet(self, dest_ip, dest_mac):
   1063         """Sends a magic packet to the NIC with the given IP and MAC addresses.
   1064 
   1065         @param dest_ip the IP address of the device to send the packet to
   1066         @param dest_mac the hardware MAC address of the device
   1067 
   1068         """
   1069         # magic packet is 6 0xff bytes followed by the hardware address
   1070         # 16 times
   1071         mac_bytes = ''.join([chr(int(b, 16)) for b in dest_mac.split(':')])
   1072         magic_packet = '\xff' * 6 + mac_bytes * 16
   1073 
   1074         logging.info('Sending magic packet to %s...', dest_ip)
   1075         self.host.run('python -uc "import socket, sys;'
   1076                       's = socket.socket(socket.AF_INET, socket.SOCK_DGRAM);'
   1077                       's.sendto(sys.stdin.read(), (\'%s\', %d))"' %
   1078                       (dest_ip, UDP_DISCARD_PORT),
   1079                       stdin=magic_packet)
   1080 
   1081 
   1082     def set_beacon_footer(self, interface, footer=''):
   1083         """Sets the beacon footer (appended IE information) for this interface.
   1084 
   1085         @param interface string interface to set the footer on.
   1086         @param footer string footer to be set on the interface.
   1087 
   1088         """
   1089         footer_file = ('/sys/kernel/debug/ieee80211/%s/beacon_footer' %
   1090                        self.iw_runner.get_interface(interface).phy)
   1091         if self.router.run('test -e %s' % footer_file,
   1092                            ignore_status=True).exit_status != 0:
   1093             logging.info('Beacon footer file does not exist.  Ignoring.')
   1094             return
   1095         self.host.run('echo -ne %s > %s' % ('%r' % footer, footer_file))
   1096 
   1097 
   1098     def setup_bridge_mode_dhcp_server(self):
   1099         """Setup an DHCP server for bridge mode.
   1100 
   1101         Setup an DHCP server on the master interface of the virtual ethernet
   1102         pair, with peer interface connected to the bridge interface. This is
   1103         used for testing APs in bridge mode.
   1104 
   1105         """
   1106         # Start a local server on master interface of virtual ethernet pair.
   1107         self.start_local_server(
   1108                 self.get_virtual_ethernet_master_interface())
   1109         # Add peer interface to the bridge.
   1110         self.add_interface_to_bridge(
   1111                 self.get_virtual_ethernet_peer_interface())
   1112