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