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