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