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