site_linux_router.py revision 6b1d9e7eb7cc38012597d1ce4dcae2a6f39175a3
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.network import interface
13from autotest_lib.client.common_lib.cros.network import netblock
14from autotest_lib.client.common_lib.cros.network import ping_runner
15from autotest_lib.server import hosts
16from autotest_lib.server import site_linux_system
17from autotest_lib.server.cros import dnsname_mangler
18from autotest_lib.server.cros import wifi_test_utils
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 = wifi_test_utils.must_be_installed(
108                    self.host, '/usr/bin/send_management_frame')
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 = wifi_test_utils.must_be_installed(
149                host, '/usr/sbin/hostapd')
150        self.cmd_hostapd_cli = wifi_test_utils.must_be_installed(
151                host, '/usr/sbin/hostapd_cli')
152        self.cmd_wpa_supplicant = wifi_test_utils.must_be_installed(
153                host, '/usr/sbin/wpa_supplicant')
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(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        self.start_hostapd(configuration)
387        interface = self.hostapd_instances[-1].interface
388        self.iw_runner.set_tx_power(interface, 'auto')
389        self.set_beacon_footer(interface, configuration.beacon_footer)
390        self.start_local_server(interface)
391        logging.info('AP configured.')
392
393
394    def ibss_configure(self, config):
395        """Configure a station based AP in IBSS mode.
396
397        Extract relevant configuration objects from |config| despite not
398        actually being a hostap managed endpoint.
399
400        @param config HostapConfig object.
401
402        """
403        if self.station_instances or self.hostapd_instances:
404            self.deconfig()
405        interface = self.get_wlanif(config.frequency, 'ibss')
406        ssid = (config.ssid or self._build_unique_ssid(config.ssid_suffix))
407        # Connect the station
408        self.router.run('%s link set %s up' % (self.cmd_ip, interface))
409        self.iw_runner.ibss_join(interface, ssid, config.frequency)
410        # Always start a local server.
411        self.start_local_server(interface)
412        # Remember that this interface is up.
413        self.station_instances.append(
414                StationInstance(ssid=ssid, interface=interface,
415                                dev_type='ibss'))
416
417
418    def local_server_address(self, index):
419        """Get the local server address for an interface.
420
421        When we multiple local servers, we give them static IP addresses
422        like 192.168.*.254.
423
424        @param index int describing which local server this is for.
425
426        """
427        return '%d.%d.%d.%d' % (self.SUBNET_PREFIX_OCTETS + (index, 254))
428
429
430    def local_peer_ip_address(self, index):
431        """Get the IP address allocated for the peer associated to the AP.
432
433        This address is assigned to a locally associated peer device that
434        is created for the DUT to perform connectivity tests with.
435        When we have multiple local servers, we give them static IP addresses
436        like 192.168.*.253.
437
438        @param index int describing which local server this is for.
439
440        """
441        return '%d.%d.%d.%d' % (self.SUBNET_PREFIX_OCTETS + (index, 253))
442
443
444    def local_peer_mac_address(self):
445        """Get the MAC address of the peer interface.
446
447        @return string MAC address of the peer interface.
448
449        """
450        iface = interface.Interface(self.station_instances[0].interface,
451                                    self.router)
452        return iface.mac_address
453
454
455    def start_local_server(self, interface):
456        """Start a local server on an interface.
457
458        @param interface string (e.g. wlan0)
459
460        """
461        logging.info('Starting up local server...')
462
463        if len(self.local_servers) >= 256:
464            raise error.TestFail('Exhausted available local servers')
465
466        server_addr = netblock.Netblock.from_addr(
467                self.local_server_address(len(self.local_servers)),
468                prefix_len=24)
469
470        params = {}
471        params['netblock'] = server_addr
472        params['dhcp_range'] = ' '.join(
473            (server_addr.get_addr_in_block(1),
474             server_addr.get_addr_in_block(128)))
475        params['interface'] = interface
476        params['ip_params'] = ('%s broadcast %s dev %s' %
477                               (server_addr.netblock,
478                                server_addr.broadcast,
479                                interface))
480        self.local_servers.append(params)
481
482        self.router.run('%s addr flush %s' %
483                        (self.cmd_ip, interface))
484        self.router.run('%s addr add %s' %
485                        (self.cmd_ip, params['ip_params']))
486        self.router.run('%s link set %s up' %
487                        (self.cmd_ip, interface))
488        self.start_dhcp_server(interface)
489
490
491    def start_dhcp_server(self, interface):
492        """Start a dhcp server on an interface.
493
494        @param interface string (e.g. wlan0)
495
496        """
497        for server in self.local_servers:
498            if server['interface'] == interface:
499                params = server
500                break
501        else:
502            raise error.TestFail('Could not find local server '
503                                 'to match interface: %r' % interface)
504        server_addr = params['netblock']
505        dhcpd_conf_file = self.dhcpd_conf % interface
506        dhcp_conf = '\n'.join([
507            'port=0',  # disables DNS server
508            'bind-interfaces',
509            'log-dhcp',
510            'dhcp-range=%s' % ','.join((server_addr.get_addr_in_block(1),
511                                        server_addr.get_addr_in_block(128))),
512            'interface=%s' % params['interface'],
513            'dhcp-leasefile=%s' % self.dhcpd_leases])
514        self.router.run('cat <<EOF >%s\n%s\nEOF\n' %
515            (dhcpd_conf_file, dhcp_conf))
516        self.router.run('dnsmasq --conf-file=%s' % dhcpd_conf_file)
517
518
519    def stop_dhcp_server(self, instance=None):
520        """Stop a dhcp server on the router.
521
522        @param instance string instance to kill.
523
524        """
525        self._kill_process_instance('dnsmasq', instance=instance)
526
527
528    def get_wifi_channel(self, ap_num):
529        """Return channel of BSS corresponding to |ap_num|.
530
531        @param ap_num int which BSS to get the channel of.
532        @return int primary channel of BSS.
533
534        """
535        instance = self.hostapd_instances[ap_num]
536        return instance.config_dict['channel']
537
538
539    def get_wifi_ip(self, ap_num):
540        """Return IP address on the WiFi subnet of a local server on the router.
541
542        If no local servers are configured (e.g. for an RSPro), a TestFail will
543        be raised.
544
545        @param ap_num int which local server to get an address from.
546
547        """
548        if not self.local_servers:
549            raise error.TestError('No IP address assigned')
550
551        return self.local_servers[ap_num]['netblock'].addr
552
553
554    def get_wifi_ip_subnet(self, ap_num):
555        """Return subnet of WiFi AP instance.
556
557        If no APs are configured a TestError will be raised.
558
559        @param ap_num int which local server to get an address from.
560
561        """
562        if not self.local_servers:
563            raise error.TestError('No APs configured.')
564
565        return self.local_servers[ap_num]['netblock'].subnet
566
567
568    def get_hostapd_interface(self, ap_num):
569        """Get the name of the interface associated with a hostapd instance.
570
571        @param ap_num: int hostapd instance number.
572        @return string interface name (e.g. 'managed0').
573
574        """
575        if ap_num not in range(len(self.hostapd_instances)):
576            raise error.TestFail('Invalid instance number (%d) with %d '
577                                 'instances configured.' %
578                                 (ap_num, len(self.hostapd_instances)))
579
580        instance = self.hostapd_instances[ap_num]
581        return instance.interface
582
583
584    def get_hostapd_mac(self, ap_num):
585        """Return the MAC address of an AP in the test.
586
587        @param ap_num int index of local server to read the MAC address from.
588        @return string MAC address like 00:11:22:33:44:55.
589
590        """
591        interface_name = self.get_hostapd_interface(ap_num)
592        ap_interface = interface.Interface(interface_name, self.host)
593        return ap_interface.mac_address
594
595
596    def get_hostapd_phy(self, ap_num):
597        """Get name of phy for hostapd instance.
598
599        @param ap_num int index of hostapd instance.
600        @return string phy name of phy corresponding to hostapd's
601                managed interface.
602
603        """
604        interface = self.iw_runner.get_interface(
605                self.get_hostapd_interface(ap_num))
606        return interface.phy
607
608
609    def deconfig(self):
610        """A legacy, deprecated alias for deconfig_aps."""
611        self.deconfig_aps()
612
613
614    def deconfig_aps(self, instance=None, silent=False):
615        """De-configure an AP (will also bring wlan down).
616
617        @param instance: int or None.  If instance is None, will bring down all
618                instances of hostapd.
619        @param silent: True if instances should be brought without de-authing
620                the DUT.
621
622        """
623        if not self.hostapd_instances and not self.station_instances:
624            return
625
626        if self.hostapd_instances:
627            local_servers = []
628            if instance is not None:
629                instances = [ self.hostapd_instances.pop(instance) ]
630                for server in self.local_servers:
631                    if server['interface'] == instances[0].interface:
632                        local_servers = [server]
633                        self.local_servers.remove(server)
634                        break
635            else:
636                instances = self.hostapd_instances
637                self.hostapd_instances = []
638                local_servers = self.local_servers
639                self.local_servers = []
640
641            for instance in instances:
642                if silent:
643                    # Deconfigure without notifying DUT.  Remove the interface
644                    # hostapd uses to send beacon and DEAUTH packets.
645                    self.remove_interface(instance.interface)
646
647                self.kill_hostapd_instance(instance)
648                self.release_interface(instance.interface)
649        if self.station_instances:
650            local_servers = self.local_servers
651            self.local_servers = []
652            instance = self.station_instances.pop()
653            if instance.dev_type == 'ibss':
654                self.iw_runner.ibss_leave(instance.interface)
655            elif instance.dev_type == 'managed':
656                self._kill_process_instance(self.cmd_wpa_supplicant,
657                                            instance=instance.interface)
658            else:
659                self.iw_runner.disconnect_station(instance.interface)
660            self.router.run('%s link set %s down' %
661                            (self.cmd_ip, instance.interface))
662
663        for server in local_servers:
664            self.stop_dhcp_server(server['interface'])
665            self.router.run("%s addr del %s" %
666                            (self.cmd_ip, server['ip_params']),
667                             ignore_status=True)
668
669
670    def confirm_pmksa_cache_use(self, instance=0):
671        """Verify that the PMKSA auth was cached on a hostapd instance.
672
673        @param instance int router instance number.
674
675        """
676        log_file = self.hostapd_instances[instance].log_file
677        pmksa_match = 'PMK from PMKSA cache'
678        result = self.router.run('grep -q "%s" %s' % (pmksa_match, log_file),
679                                 ignore_status=True)
680        if result.exit_status:
681            raise error.TestFail('PMKSA cache was not used in roaming.')
682
683
684    def get_ssid(self, instance=None):
685        """@return string ssid for the network stemming from this router."""
686        if instance is None:
687            instance = 0
688            if len(self.hostapd_instances) > 1:
689                raise error.TestFail('No instance of hostapd specified with '
690                                     'multiple instances present.')
691
692        if self.hostapd_instances:
693            return self.hostapd_instances[instance].ssid
694
695        if self.station_instances:
696            return self.station_instances[0].ssid
697
698        raise error.TestFail('Requested ssid of an unconfigured AP.')
699
700
701    def deauth_client(self, client_mac):
702        """Deauthenticates a client described in params.
703
704        @param client_mac string containing the mac address of the client to be
705               deauthenticated.
706
707        """
708        control_if = self.hostapd_instances[-1].config_dict['ctrl_interface']
709        self.router.run('%s -p%s deauthenticate %s' %
710                        (self.cmd_hostapd_cli, control_if, client_mac))
711
712
713    def send_management_frame_on_ap(self, frame_type, channel, instance=0):
714        """Injects a management frame into an active hostapd session.
715
716        @param frame_type string the type of frame to send.
717        @param channel int targeted channel
718        @param instance int indicating which hostapd instance to inject into.
719
720        """
721        hostap_interface = self.hostapd_instances[instance].interface
722        interface = self.get_wlanif(0, 'monitor', same_phy_as=hostap_interface)
723        self.router.run("%s link set %s up" % (self.cmd_ip, interface))
724        self.router.run('%s -i %s -t %s -c %d' %
725                        (self.cmd_send_management_frame, interface, frame_type,
726                         channel))
727        self.release_interface(interface)
728
729
730    def setup_management_frame_interface(self, channel):
731        """
732        Setup interface for injecting management frames.
733
734        @param channel int channel to inject the frames.
735
736        @return string name of the interface.
737
738        """
739        frequency = hostap_config.HostapConfig.get_frequency_for_channel(
740                channel)
741        interface = self.get_wlanif(frequency, 'monitor')
742        self.router.run('%s link set %s up' % (self.cmd_ip, interface))
743        self.iw_runner.set_freq(interface, frequency)
744        return interface
745
746
747    def send_management_frame(self, interface, frame_type, channel,
748                              ssid_prefix=None, num_bss=None,
749                              frame_count=None, delay=None):
750        """
751        Injects management frames on specify channel |frequency|.
752
753        This function will spawn off a new process to inject specified
754        management frames |frame_type| at the specified interface |interface|.
755
756        @param interface string interface to inject frames.
757        @param frame_type string message type.
758        @param channel int targeted channel.
759        @param ssid_prefix string SSID prefix.
760        @param num_bss int number of BSS.
761        @param frame_count int number of frames to send.
762        @param delay int milliseconds delay between frames.
763
764        @return int PID of the newly created process.
765
766        """
767        command = '%s -i %s -t %s -c %d' % (self.cmd_send_management_frame,
768                                interface, frame_type, channel)
769        if ssid_prefix is not None:
770            command += ' -s %s' % (ssid_prefix)
771        if num_bss is not None:
772            command += ' -b %d' % (num_bss)
773        if frame_count is not None:
774            command += ' -n %d' % (frame_count)
775        if delay is not None:
776            command += ' -d %d' % (delay)
777        command += ' > %s 2>&1 & echo $!' % (self.MGMT_FRAME_SENDER_LOG_FILE)
778        pid = int(self.router.run(command).stdout)
779        return pid
780
781
782    def detect_client_deauth(self, client_mac, instance=0):
783        """Detects whether hostapd has logged a deauthentication from
784        |client_mac|.
785
786        @param client_mac string the MAC address of the client to detect.
787        @param instance int indicating which hostapd instance to query.
788
789        """
790        interface = self.hostapd_instances[instance].interface
791        deauth_msg = "%s: deauthentication: STA=%s" % (interface, client_mac)
792        log_file = self.hostapd_instances[instance].log_file
793        result = self.router.run("grep -qi '%s' %s" % (deauth_msg, log_file),
794                                 ignore_status=True)
795        return result.exit_status == 0
796
797
798    def detect_client_coexistence_report(self, client_mac, instance=0):
799        """Detects whether hostapd has logged an action frame from
800        |client_mac| indicating information about 20/40MHz BSS coexistence.
801
802        @param client_mac string the MAC address of the client to detect.
803        @param instance int indicating which hostapd instance to query.
804
805        """
806        coex_msg = ('nl80211: MLME event frame - hexdump(len=.*): '
807                    '.. .. .. .. .. .. .. .. .. .. %s '
808                    '.. .. .. .. .. .. .. .. 04 00.*48 01 ..' %
809                    ' '.join(client_mac.split(':')))
810        log_file = self.hostapd_instances[instance].log_file
811        result = self.router.run("grep -qi '%s' %s" % (coex_msg, log_file),
812                                 ignore_status=True)
813        return result.exit_status == 0
814
815
816    def add_connected_peer(self, instance=0):
817        """Configure a station connected to a running AP instance.
818
819        Extract relevant configuration objects from the hostap
820        configuration for |instance| and generate a wpa_supplicant
821        instance that connects to it.  This allows the DUT to interact
822        with a client entity that is also connected to the same AP.  A
823        full wpa_supplicant instance is necessary here (instead of just
824        using the "iw" command to connect) since we want to enable
825        advanced features such as TDLS.
826
827        @param instance int indicating which hostapd instance to connect to.
828
829        """
830        if not self.hostapd_instances:
831            raise error.TestFail('Hostapd is not configured.')
832
833        if self.station_instances:
834            raise error.TestFail('Station is already configured.')
835
836        ssid = self.get_ssid(instance)
837        hostap_conf = self.hostapd_instances[instance].config_dict
838        frequency = hostap_config.HostapConfig.get_frequency_for_channel(
839                hostap_conf['channel'])
840        interface = self.get_wlanif(frequency, 'managed')
841
842        # TODO(pstew): Configure other bits like PSK, 802.11n if tests
843        # require them...
844        supplicant_config = (
845                'network={\n'
846                '  ssid="%(ssid)s"\n'
847                '  key_mgmt=NONE\n'
848                '}\n' % {'ssid': ssid}
849        )
850
851        conf_file = self.STATION_CONF_FILE_PATTERN % interface
852        log_file = self.STATION_LOG_FILE_PATTERN % interface
853        pid_file = self.STATION_PID_FILE_PATTERN % interface
854
855        self.router.run('cat <<EOF >%s\n%s\nEOF\n' %
856            (conf_file, supplicant_config))
857
858        # Connect the station.
859        self.router.run('%s link set %s up' % (self.cmd_ip, interface))
860        start_command = ('%s -dd -t -i%s -P%s -c%s -D%s &> %s &' %
861                         (self.cmd_wpa_supplicant,
862                         interface, pid_file, conf_file,
863                         self.HOSTAPD_DRIVER_NAME, log_file))
864        self.router.run(start_command)
865        self.iw_runner.wait_for_link(interface)
866
867        # Assign an IP address to this interface.
868        self.router.run('%s addr add %s/24 dev %s' %
869                        (self.cmd_ip, self.local_peer_ip_address(instance),
870                         interface))
871
872        # Since we now have two network interfaces connected to the same
873        # network, we need to disable the kernel's protection against
874        # incoming packets to an "unexpected" interface.
875        self.router.run('echo 2 > /proc/sys/net/ipv4/conf/%s/rp_filter' %
876                        interface)
877
878        # Similarly, we'd like to prevent the hostap interface from
879        # replying to ARP requests for the peer IP address and vice
880        # versa.
881        self.router.run('echo 1 > /proc/sys/net/ipv4/conf/%s/arp_ignore' %
882                        interface)
883        self.router.run('echo 1 > /proc/sys/net/ipv4/conf/%s/arp_ignore' %
884                        hostap_conf['interface'])
885
886        self.station_instances.append(
887                StationInstance(ssid=ssid, interface=interface,
888                                dev_type='managed'))
889
890
891    def send_magic_packet(self, dest_ip, dest_mac):
892        """Sends a magic packet to the NIC with the given IP and MAC addresses.
893
894        @param dest_ip the IP address of the device to send the packet to
895        @param dest_mac the hardware MAC address of the device
896
897        """
898        # magic packet is 6 0xff bytes followed by the hardware address
899        # 16 times
900        mac_bytes = ''.join([chr(int(b, 16)) for b in dest_mac.split(':')])
901        magic_packet = '\xff' * 6 + mac_bytes * 16
902
903        logging.info('Sending magic packet to %s...', dest_ip)
904        self.host.run('python -uc "import socket, sys;'
905                      's = socket.socket(socket.AF_INET, socket.SOCK_DGRAM);'
906                      's.sendto(sys.stdin.read(), (\'%s\', %d))"' %
907                      (dest_ip, UDP_DISCARD_PORT),
908                      stdin=magic_packet)
909
910
911    def set_beacon_footer(self, interface, footer=''):
912        """Sets the beacon footer (appended IE information) for this interface.
913
914        @param interface string interface to set the footer on.
915        @param footer string footer to be set on the interface.
916
917        """
918        footer_file = ('/sys/kernel/debug/ieee80211/%s/beacon_footer' %
919                       self.iw_runner.get_interface(interface).phy)
920        if self.router.run('test -e %s' % footer_file,
921                           ignore_status=True).exit_status != 0:
922            logging.info('Beacon footer file does not exist.  Ignoring.')
923            return
924        self.host.run('echo -ne %s > %s' % ('%r' % footer, footer_file))
925