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