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