site_linux_router.py revision 5689d3618fd41cb5951a5fc2cc5f772c8997dab9
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 logging
6import random
7import string
8import time
9
10from autotest_lib.client.common_lib import error
11from autotest_lib.client.common_lib.cros.network import interface
12from autotest_lib.server import site_linux_system
13from autotest_lib.server.cros import wifi_test_utils
14from autotest_lib.server.cros.network import hostap_config
15
16class LinuxRouter(site_linux_system.LinuxSystem):
17    """Linux/mac80211-style WiFi Router support for WiFiTest class.
18
19    This class implements test methods/steps that communicate with a
20    router implemented with Linux/mac80211.  The router must
21    be pre-configured to enable ssh access and have a mac80211-based
22    wireless device.  We also assume hostapd 0.7.x and iw are present
23    and any necessary modules are pre-loaded.
24
25    """
26
27    KNOWN_TEST_PREFIX = 'network_WiFi'
28    STARTUP_POLLING_INTERVAL_SECONDS = 0.5
29    STARTUP_TIMEOUT_SECONDS = 10
30    SUFFIX_LETTERS = string.ascii_lowercase + string.digits
31    SUBNET_PREFIX_OCTETS = (192, 168)
32
33    HOSTAPD_CONF_FILE_PATTERN = '/tmp/hostapd-test-%s.conf'
34    HOSTAPD_LOG_FILE_PATTERN = '/tmp/hostapd-test-%s.log'
35    HOSTAPD_PID_FILE_PATTERN = '/tmp/hostapd-test-%s.pid'
36    HOSTAPD_DRIVER_NAME = 'nl80211'
37
38    STATION_CONF_FILE_PATTERN = '/tmp/wpa-supplicant-test-%s.conf'
39    STATION_LOG_FILE_PATTERN = '/tmp/wpa-supplicant-test-%s.log'
40    STATION_PID_FILE_PATTERN = '/tmp/wpa-supplicant-test-%s.pid'
41
42    def get_capabilities(self):
43        """@return iterable object of AP capabilities for this system."""
44        caps = set([self.CAPABILITY_IBSS])
45        try:
46            self.cmd_send_management_frame = wifi_test_utils.must_be_installed(
47                    self.router, '/usr/bin/send_management_frame')
48            caps.add(self.CAPABILITY_SEND_MANAGEMENT_FRAME)
49        except error.TestFail:
50            pass
51        return super(LinuxRouter, self).get_capabilities().union(caps)
52
53
54    def __init__(self, host, params, test_name):
55        """Build a LinuxRouter.
56
57        @param host Host object representing the remote machine.
58        @param params dict of settings from site_wifitest based tests.
59        @param test_name string name of this test.  Used in SSID creation.
60
61        """
62        params = params.copy()
63        params.update({
64            'phy_bus_preference': {
65                'monitor': 'usb',
66                'managed': 'pci'
67            }})
68        site_linux_system.LinuxSystem.__init__(self, host, params, 'router')
69
70        # Router host.
71        self.router = host
72
73        self.cmd_dhcpd = '/usr/sbin/dhcpd'
74        self.cmd_hostapd = wifi_test_utils.must_be_installed(
75                host, '/usr/sbin/hostapd')
76        self.cmd_hostapd_cli = wifi_test_utils.must_be_installed(
77                host, '/usr/sbin/hostapd_cli')
78        self.cmd_wpa_supplicant = wifi_test_utils.must_be_installed(
79                host, '/usr/sbin/wpa_supplicant')
80        self.dhcpd_conf = '/tmp/dhcpd.%s.conf'
81        self.dhcpd_leases = '/tmp/dhcpd.leases'
82
83        # hostapd configuration persists throughout the test, subsequent
84        # 'config' commands only modify it.
85        self.ssid_prefix = test_name
86        if self.ssid_prefix.startswith(self.KNOWN_TEST_PREFIX):
87            # Many of our tests start with an uninteresting prefix.
88            # Remove it so we can have more unique bytes.
89            self.ssid_prefix = self.ssid_prefix[len(self.KNOWN_TEST_PREFIX):]
90        self.ssid_prefix = self.ssid_prefix.lstrip('_')
91        self.ssid_prefix += '_'
92
93        self._total_hostapd_instances = 0
94        self.station = {
95            'configured': False,
96            'config_file': "/tmp/wpa-supplicant-test-%s.conf",
97            'log_file': "/tmp/wpa-supplicant-test-%s.log",
98            'pid_file': "/tmp/wpa-supplicant-test-%s.pid",
99            'conf': {},
100        }
101        self.local_servers = []
102        self.hostapd_instances = []
103        self.dhcp_low = 1
104        self.dhcp_high = 128
105
106        # Kill hostapd and dhcp server if already running.
107        self.kill_hostapd()
108        self.stop_dhcp_servers()
109
110        # Place us in the US by default
111        self.iw_runner.set_regulatory_domain('US')
112
113
114    def close(self):
115        """Close global resources held by this system."""
116        self.destroy()
117        super(LinuxRouter, self).close()
118
119
120    def create_wifi_device(self, device_type='hostap'):
121        """Create a wifi device of the specified type.
122
123        Defaults to creating a hostap managed device.
124
125        @param device_type string device type.
126
127        """
128        #
129        # AP mode is handled entirely by hostapd so we only
130        # have to setup others (mapping the bsd type to what
131        # iw wants)
132        #
133        # map from bsd types to iw types
134        self.apmode = device_type in ('ap', 'hostap')
135        if not self.apmode:
136            self.station['type'] = device_type
137        self.phytype = {
138            'sta'       : 'managed',
139            'monitor'   : 'monitor',
140            'adhoc'     : 'adhoc',
141            'ibss'      : 'ibss',
142            'ap'        : 'managed',     # NB: handled by hostapd
143            'hostap'    : 'managed',     # NB: handled by hostapd
144            'mesh'      : 'mesh',
145            'wds'       : 'wds',
146        }[device_type]
147
148
149    def destroy(self):
150        """Destroy a previously created device."""
151        self.deconfig()
152
153
154    def has_local_server(self):
155        """@return True iff this router has local servers configured."""
156        return bool(self.local_servers)
157
158
159    def start_hostapd(self, hostapd_conf_dict, configuration):
160        """Start a hostapd instance described by conf.
161
162        @param hostapd_conf_dict dict of hostapd configuration parameters.
163        @param configuration HostapConfig object.
164
165        """
166        logging.info('Starting hostapd with parameters: %r',
167                     hostapd_conf_dict)
168        # Figure out the correct interface.
169        interface = self.get_wlanif(configuration.frequency,
170                                    self.phytype,
171                                    configuration.hw_mode)
172
173        conf_file = self.HOSTAPD_CONF_FILE_PATTERN % interface
174        log_file = self.HOSTAPD_LOG_FILE_PATTERN % interface
175        pid_file = self.HOSTAPD_PID_FILE_PATTERN % interface
176        hostapd_conf_dict['interface'] = interface
177
178        # Generate hostapd.conf.
179        self.router.run("cat <<EOF >%s\n%s\nEOF\n" %
180            (conf_file, '\n'.join(
181            "%s=%s" % kv for kv in hostapd_conf_dict.iteritems())))
182
183        # Run hostapd.
184        logging.info("Starting hostapd...")
185        self.router.run('rm %s' % log_file, ignore_status=True)
186        self.router.run('rm %s' % pid_file, ignore_status=True)
187        self.router.run('stop wpasupplicant', ignore_status=True)
188        start_command = '%s -dd -B -t -f %s -P %s %s' % (
189                self.cmd_hostapd, log_file, pid_file, conf_file)
190        self.router.run(start_command)
191        self.hostapd_instances.append({
192            'ssid': hostapd_conf_dict['ssid'],
193            'conf_file': conf_file,
194            'log_file': log_file,
195            'interface': interface,
196            'pid_file': pid_file,
197            'config_dict': hostapd_conf_dict.copy()
198        })
199
200        # Wait for confirmation that the router came up.
201        pid = int(self.router.run('cat %s' % pid_file).stdout)
202        logging.info('Waiting for hostapd to startup.')
203        start_time = time.time()
204        while time.time() - start_time < self.STARTUP_TIMEOUT_SECONDS:
205            success = self.router.run(
206                    'grep "Completing interface initialization" %s' % log_file,
207                    ignore_status=True).exit_status == 0
208            if success:
209                break
210
211            # A common failure is an invalid router configuration.
212            # Detect this and exit early if we see it.
213            bad_config = self.router.run(
214                    'grep "Interface initialization failed" %s' % log_file,
215                    ignore_status=True).exit_status == 0
216            if bad_config:
217                raise error.TestFail('hostapd failed to initialize AP '
218                                     'interface.')
219
220            if pid:
221                early_exit = self.router.run('kill -0 %d' % pid,
222                                             ignore_status=True).exit_status
223                if early_exit:
224                    raise error.TestFail('hostapd process terminated.')
225
226            time.sleep(self.STARTUP_POLLING_INTERVAL_SECONDS)
227        else:
228            raise error.TestFail('Timed out while waiting for hostapd '
229                                 'to start.')
230
231
232    def _kill_process_instance(self, process, instance=None, wait=0):
233        """Kill a process on the router.
234
235        Kills program named |process|, optionally only a specific
236        |instance|.  If |wait| is specified, we makes sure |process| exits
237        before returning.
238
239        @param process string name of process to kill.
240        @param instance string instance of process to kill.
241        @param wait int timeout in seconds to wait for.
242
243        """
244        if instance:
245            search_arg = '-f "%s.*%s"' % (process, instance)
246        else:
247            search_arg = process
248
249        cmd = "pkill %s >/dev/null 2>&1" % search_arg
250
251        if wait:
252            cmd += (" && while pgrep %s &> /dev/null; do sleep 1; done" %
253                    search_arg)
254            self.router.run(cmd, timeout=wait, ignore_status=True)
255        else:
256            self.router.run(cmd, ignore_status=True)
257
258
259    def kill_hostapd_instance(self, instance):
260        """Kills a hostapd instance.
261
262        @param instance string instance to kill.
263
264        """
265        self._kill_process_instance('hostapd', instance, 30)
266
267
268    def kill_hostapd(self):
269        """Kill all hostapd instances."""
270        self.kill_hostapd_instance(None)
271
272
273    def __get_default_hostap_config(self):
274        """@return dict of default options for hostapd."""
275        return {'hw_mode': 'g',
276                'ctrl_interface': '/tmp/hostapd-test.control',
277                'logger_syslog': '-1',
278                'logger_syslog_level': '0',
279                # default RTS and frag threshold to ``off''
280                'rts_threshold': '2347',
281                'fragm_threshold': '2346',
282                'driver': self.HOSTAPD_DRIVER_NAME,
283                'ssid': self._build_ssid('') }
284
285
286    def _build_ssid(self, suffix):
287        unique_salt = ''.join([random.choice(self.SUFFIX_LETTERS)
288                               for x in range(5)])
289        return (self.ssid_prefix + unique_salt + suffix)[-32:]
290
291
292    def hostap_configure(self, configuration, multi_interface=None):
293        """Build up a hostapd configuration file and start hostapd.
294
295        Also setup a local server if this router supports them.
296
297        @param configuration HosetapConfig object.
298        @param multi_interface bool True iff multiple interfaces allowed.
299
300        """
301        if multi_interface is None and (self.hostapd_instances or
302                                        self.station['configured']):
303            self.deconfig()
304        # Start with the default hostapd config parameters.
305        conf = self.__get_default_hostap_config()
306        conf['ssid'] = (configuration.ssid or
307                        self._build_ssid(configuration.ssid_suffix))
308        if configuration.bssid:
309            conf['bssid'] = configuration.bssid
310        conf['channel'] = configuration.channel
311        conf['hw_mode'] = configuration.hw_mode
312        if configuration.hide_ssid:
313            conf['ignore_broadcast_ssid'] = 1
314        if configuration.is_11n:
315            conf['ieee80211n'] = 1
316            conf['ht_capab'] = configuration.hostapd_ht_capabilities
317        if configuration.wmm_enabled:
318            conf['wmm_enabled'] = 1
319        if configuration.require_ht:
320            conf['require_ht'] = 1
321        if configuration.beacon_interval:
322            conf['beacon_int'] = configuration.beacon_interval
323        if configuration.dtim_period:
324            conf['dtim_period'] = configuration.dtim_period
325        if configuration.frag_threshold:
326            conf['fragm_threshold'] = configuration.frag_threshold
327        if configuration.pmf_support:
328            conf['ieee80211w'] = configuration.pmf_support
329        if configuration.obss_interval:
330            conf['obss_interval'] = configuration.obss_interval
331        conf.update(configuration.get_security_hostapd_conf())
332        self.start_hostapd(conf, configuration)
333        interface = self.hostapd_instances[-1]['interface']
334        self.iw_runner.set_tx_power(interface, 'auto')
335        self.start_local_server(interface)
336        logging.info('AP configured.')
337
338
339    @staticmethod
340    def ip_addr(netblock, idx):
341        """Simple IPv4 calculator.
342
343        Takes host address in "IP/bits" notation and returns netmask, broadcast
344        address as well as integer offsets into the address range.
345
346        @param netblock string host address in "IP/bits" notation.
347        @param idx string describing what to return.
348        @return string containing something you hopefully requested.
349
350        """
351        addr_str,bits = netblock.split('/')
352        addr = map(int, addr_str.split('.'))
353        mask_bits = (-1 << (32-int(bits))) & 0xffffffff
354        mask = [(mask_bits >> s) & 0xff for s in range(24, -1, -8)]
355        if idx == 'local':
356            return addr_str
357        elif idx == 'netmask':
358            return '.'.join(map(str, mask))
359        elif idx == 'broadcast':
360            offset = [m ^ 0xff for m in mask]
361        else:
362            offset = [(idx >> s) & 0xff for s in range(24, -1, -8)]
363        return '.'.join(map(str, [(a & m) + o
364                                  for a, m, o in zip(addr, mask, offset)]))
365
366
367    def ibss_configure(self, config):
368        """Configure a station based AP in IBSS mode.
369
370        Extract relevant configuration objects from |config| despite not
371        actually being a hostap managed endpoint.
372
373        @param config HostapConfig object.
374
375        """
376        if self.station['configured'] or self.hostapd_instances:
377            self.deconfig()
378        interface = self.get_wlanif(config.frequency, self.phytype,
379                                    config.hw_mode)
380        self.station['conf']['ssid'] = (config.ssid or
381                                        self._build_ssid(config.ssid_suffix))
382        # Connect the station
383        self.router.run('%s link set %s up' % (self.cmd_ip, interface))
384        self.iw_runner.ibss_join(
385                interface, self.station['conf']['ssid'], config.frequency)
386        # Always start a local server.
387        self.start_local_server(interface)
388        # Remember that this interface is up.
389        self.station['configured'] = True
390        self.station['interface'] = interface
391
392
393    def local_server_address(self, index):
394        """Get the local server address for an interface.
395
396        When we multiple local servers, we give them static IP addresses
397        like 192.168.*.254.
398
399        @param index int describing which local server this is for.
400
401        """
402        return '%d.%d.%d.%d' % (self.SUBNET_PREFIX_OCTETS + (index, 254))
403
404
405    def local_peer_ip_address(self, index):
406        """Get the IP address allocated for the peer associated to the AP.
407
408        This address is assigned to a locally associated peer device that
409        is created for the DUT to perform connectivity tests with.
410        When we have multiple local servers, we give them static IP addresses
411        like 192.168.*.253.
412
413        @param index int describing which local server this is for.
414
415        """
416        return '%d.%d.%d.%d' % (self.SUBNET_PREFIX_OCTETS + (index, 253))
417
418
419    def local_peer_mac_address(self):
420        """Get the MAC address of the peer interface.
421
422        @return string MAC address of the peer interface.
423        """
424        iface = interface.Interface(self.station['interface'], self.router)
425        return iface.mac_address
426
427
428    def start_local_server(self, interface):
429        """Start a local server on an interface.
430
431        @param interface string (e.g. wlan0)
432
433        """
434        logging.info("Starting up local server...")
435
436        if len(self.local_servers) >= 256:
437            raise error.TestFail('Exhausted available local servers')
438
439        netblock = '%s/24' % self.local_server_address(len(self.local_servers))
440
441        params = {}
442        params['netblock'] = netblock
443        params['subnet'] = self.ip_addr(netblock, 0)
444        params['netmask'] = self.ip_addr(netblock, 'netmask')
445        params['dhcp_range'] = ' '.join(
446            (self.ip_addr(netblock, self.dhcp_low),
447             self.ip_addr(netblock, self.dhcp_high)))
448        params['interface'] = interface
449
450        params['ip_params'] = ("%s broadcast %s dev %s" %
451                               (netblock,
452                                self.ip_addr(netblock, 'broadcast'),
453                                interface))
454        self.local_servers.append(params)
455
456        self.router.run("%s addr flush %s" %
457                        (self.cmd_ip, interface))
458        self.router.run("%s addr add %s" %
459                        (self.cmd_ip, params['ip_params']))
460        self.router.run("%s link set %s up" %
461                        (self.cmd_ip, interface))
462        self.start_dhcp_server(interface)
463
464
465    def start_dhcp_server(self, interface):
466        """Start a dhcp server on an interface.
467
468        @param interface string (e.g. wlan0)
469
470        """
471        for server in self.local_servers:
472            if server['interface'] == interface:
473                params = server
474                break
475        else:
476            raise error.TestFail('Could not find local server '
477                                 'to match interface: %r' % interface)
478
479        dhcpd_conf_file = self.dhcpd_conf % interface
480        dhcp_conf = '\n'.join([
481            'port=0',  # disables DNS server
482            'bind-interfaces',
483            'log-dhcp',
484            'dhcp-range=%s' % params['dhcp_range'].replace(' ', ','),
485            'interface=%s' % params['interface'],
486            'dhcp-leasefile=%s' % self.dhcpd_leases])
487        self.router.run('cat <<EOF >%s\n%s\nEOF\n' %
488            (dhcpd_conf_file, dhcp_conf))
489        self.router.run('dnsmasq --conf-file=%s' % dhcpd_conf_file)
490
491
492    def stop_dhcp_server(self, instance=None):
493        """Stop a dhcp server on the router.
494
495        @param instance string instance to kill.
496
497        """
498        self._kill_process_instance('dnsmasq', instance, 0)
499
500
501    def stop_dhcp_servers(self):
502        """Stop all dhcp servers on the router."""
503        self.stop_dhcp_server(None)
504
505
506    def get_wifi_channel(self, ap_num):
507        """Return channel of BSS corresponding to |ap_num|.
508
509        @param ap_num int which BSS to get the channel of.
510        @return int primary channel of BSS.
511
512        """
513        instance = self.hostapd_instances[ap_num]
514        return instance['config_dict']['channel']
515
516
517    def get_wifi_ip(self, ap_num):
518        """Return IP address on the WiFi subnet of a local server on the router.
519
520        If no local servers are configured (e.g. for an RSPro), a TestFail will
521        be raised.
522
523        @param ap_num int which local server to get an address from.
524
525        """
526        if self.local_servers:
527            return self.ip_addr(self.local_servers[ap_num]['netblock'],
528                                'local')
529        else:
530            raise error.TestFail("No IP address assigned")
531
532
533    def get_hostapd_mac(self, ap_num):
534        """Return the MAC address of an AP in the test.
535
536        @param ap_num int index of local server to read the MAC address from.
537        @return string MAC address like 00:11:22:33:44:55.
538
539        """
540        if not self.local_servers:
541            raise error.TestFail('Cannot retrieve MAC: '
542                                 'no AP instances configured.')
543
544        instance = self.hostapd_instances[ap_num]
545        ap_interface = interface.Interface(instance['interface'], self.host)
546        return ap_interface.mac_address
547
548
549    def deconfig(self):
550        """A legacy, deprecated alias for deconfig_aps."""
551        self.deconfig_aps()
552
553
554    def deconfig_aps(self, instance=None, silent=False):
555        """De-configure an AP (will also bring wlan down).
556
557        @param instance: int or None.  If instance is None, will bring down all
558                instances of hostapd.
559        @param silent: True if instances should be brought without de-authing
560                the DUT.
561
562        """
563        if not self.hostapd_instances and not self.station['configured']:
564            return
565
566        if self.hostapd_instances:
567            local_servers = []
568            if instance is not None:
569                instances = [ self.hostapd_instances.pop(instance) ]
570                for server in self.local_servers:
571                    if server['interface'] == instances[0]['interface']:
572                        local_servers = [server]
573                        self.local_servers.remove(server)
574                        break
575            else:
576                instances = self.hostapd_instances
577                self.hostapd_instances = []
578                local_servers = self.local_servers
579                self.local_servers = []
580
581            for instance in instances:
582                if silent:
583                    # Deconfigure without notifying DUT.  Remove the interface
584                    # hostapd uses to send beacon and DEAUTH packets.
585                    self.remove_interface(instance['interface'])
586
587                self.kill_hostapd_instance(instance['conf_file'])
588                if wifi_test_utils.is_installed(self.host,
589                                                instance['log_file']):
590                    self.router.get_file(instance['log_file'],
591                                         'debug/hostapd_router_%d_%s.log' %
592                                         (self._total_hostapd_instances,
593                                          instance['interface']))
594                else:
595                    logging.error('Did not collect hostapd log file because '
596                                  'it was missing.')
597                self.release_interface(instance['interface'])
598#               self.router.run("rm -f %(log_file)s %(conf_file)s" % instance)
599            self._total_hostapd_instances += 1
600        if self.station['configured']:
601            local_servers = self.local_servers
602            self.local_servers = []
603            if self.station['type'] == 'ibss':
604                self.iw_runner.ibss_leave(self.station['interface'])
605            elif self.station['type'] == 'supplicant':
606                self._kill_process_instance('wpa_supplicant',
607                                            self.station['interface'])
608            else:
609                self.iw_runner.disconnect_station(self.station['interface'])
610            self.router.run("%s link set %s down" % (self.cmd_ip,
611                                                     self.station['interface']))
612
613        for server in local_servers:
614            self.stop_dhcp_server(server['interface'])
615            self.router.run("%s addr del %s" %
616                            (self.cmd_ip, server['ip_params']),
617                             ignore_status=True)
618
619        self.station['configured'] = False
620
621
622    def confirm_pmksa_cache_use(self, instance=0):
623        """Verify that the PMKSA auth was cached on a hostapd instance.
624
625        @param instance int router instance number.
626
627        """
628        log_file = self.hostapd_instances[instance]['log_file']
629        pmksa_match = 'PMK from PMKSA cache'
630        result = self.router.run('grep -q "%s" %s' % (pmksa_match, log_file),
631                                 ignore_status=True)
632        if result.exit_status:
633            raise error.TestFail('PMKSA cache was not used in roaming.')
634
635
636    def get_ssid(self, instance=None):
637        """@return string ssid for the network stemming from this router."""
638        if instance is None:
639            instance = 0
640            if len(self.hostapd_instances) > 1:
641                raise error.TestFail('No instance of hostapd specified with '
642                                     'multiple instances present.')
643
644        if self.hostapd_instances:
645            return self.hostapd_instances[instance]['ssid']
646
647        if not 'ssid' in self.station['conf']:
648            raise error.TestFail('Requested ssid of an unconfigured AP.')
649
650        return self.station['conf']['ssid']
651
652
653    def deauth_client(self, client_mac):
654        """Deauthenticates a client described in params.
655
656        @param client_mac string containing the mac address of the client to be
657               deauthenticated.
658
659        """
660        self.router.run('%s -p%s deauthenticate %s' %
661                        (self.cmd_hostapd_cli,
662                         self.hostapd_instances[-1]['ctrl_interface'],
663                         client_mac))
664
665
666    def send_management_frame(self, frame_type, instance=0):
667        """Injects a management frame into an active hostapd session.
668
669        @param frame_type string the type of frame to send.
670        @param instance int indicating which hostapd instance to inject into.
671
672        """
673        hostap_interface = self.hostapd_instances[instance]['interface']
674        interface = self.get_wlanif(0, 'monitor', same_phy_as=hostap_interface)
675        self.router.run("%s link set %s up" % (self.cmd_ip, interface))
676        self.router.run('%s %s %s' %
677                        (self.cmd_send_management_frame, interface, frame_type))
678        self.release_interface(interface)
679
680
681    def detect_client_deauth(self, client_mac, instance=0):
682        """Detects whether hostapd has logged a deauthentication from
683        |client_mac|.
684
685        @param client_mac string the MAC address of the client to detect.
686        @param instance int indicating which hostapd instance to query.
687
688        """
689        interface = self.hostapd_instances[instance]['interface']
690        deauth_msg = "%s: deauthentication: STA=%s" % (interface, client_mac)
691        log_file = self.hostapd_instances[instance]['log_file']
692        result = self.router.run("grep -qi '%s' %s" % (deauth_msg, log_file),
693                                 ignore_status=True)
694        return result.exit_status == 0
695
696
697    def detect_client_coexistence_report(self, client_mac, instance=0):
698        """Detects whether hostapd has logged an action frame from
699        |client_mac| indicating information about 20/40MHz BSS coexistence.
700
701        @param client_mac string the MAC address of the client to detect.
702        @param instance int indicating which hostapd instance to query.
703
704        """
705        coex_msg = ('nl80211: MLME event frame - hexdump(len=.*): '
706                    '.. .. .. .. .. .. .. .. .. .. %s '
707                    '.. .. .. .. .. .. .. .. 04 00.*48 01 ..' %
708                    ' '.join(client_mac.split(':')))
709        log_file = self.hostapd_instances[instance]['log_file']
710        result = self.router.run("grep -qi '%s' %s" % (coex_msg, log_file),
711                                 ignore_status=True)
712        return result.exit_status == 0
713
714
715    def add_connected_peer(self, instance=0):
716        """Configure a station connected to a running AP instance.
717
718        Extract relevant configuration objects from the hostap
719        configuration for |instance| and generate a wpa_supplicant
720        instance that connects to it.  This allows the DUT to interact
721        with a client entity that is also connected to the same AP.  A
722        full wpa_supplicant instance is necessary here (instead of just
723        using the "iw" command to connect) since we want to enable
724        advanced features such as TDLS.
725
726        @param instance int indicating which hostapd instance to connect to.
727
728        """
729        if not self.hostapd_instances:
730            raise error.TestFail('Hostapd is not configured.')
731
732        if self.station['configured']:
733            raise error.TestFail('Station is already configured.')
734
735        client_conf = self.station['conf']
736        client_conf['ssid'] = self.get_ssid(instance)
737
738        hostap_conf = self.hostapd_instances[instance]['config_dict']
739        frequency = hostap_config.HostapConfig.get_frequency_for_channel(
740                hostap_conf['channel'])
741        interface = self.get_wlanif(
742                frequency, 'managed', hostap_conf['hw_mode'])
743        client_conf['interface'] = interface
744
745        # TODO(pstew): Configure other bits like PSK, 802.11n if tests
746        # require them...
747        supplicant_config = (
748                'network={\n'
749                '  ssid="%(ssid)s"\n'
750                '  key_mgmt=NONE\n'
751                '}\n' % client_conf
752        )
753
754        conf_file = self.STATION_CONF_FILE_PATTERN % interface
755        log_file = self.STATION_LOG_FILE_PATTERN % interface
756        pid_file = self.STATION_PID_FILE_PATTERN % interface
757
758        self.router.run('cat <<EOF >%s\n%s\nEOF\n' %
759            (conf_file, supplicant_config))
760
761        # Connect the station.
762        self.router.run('%s link set %s up' % (self.cmd_ip, interface))
763        start_command = ('%s -dd -t -i%s -P%s -c%s -D%s &> %s &' %
764                         (self.cmd_wpa_supplicant,
765                         interface, pid_file, conf_file,
766                         self.HOSTAPD_DRIVER_NAME, log_file))
767        self.router.run(start_command)
768        self.iw_runner.wait_for_link(interface)
769
770        # Assign an IP address to this interface.
771        self.router.run('%s addr add %s/24 dev %s' %
772                        (self.cmd_ip, self.local_peer_ip_address(instance),
773                         interface))
774
775        # Since we now have two network interfaces connected to the same
776        # network, we need to disable the kernel's protection against
777        # incoming packets to an "unexpected" interface.
778        self.router.run('echo 2 > /proc/sys/net/ipv4/conf/%s/rp_filter' %
779                        interface)
780
781        # Similarly, we'd like to prevent the hostap interface from
782        # replying to ARP requests for the peer IP address and vice
783        # versa.
784        self.router.run('echo 1 > /proc/sys/net/ipv4/conf/%s/arp_ignore' %
785                        interface)
786        self.router.run('echo 1 > /proc/sys/net/ipv4/conf/%s/arp_ignore' %
787                        hostap_conf['interface'])
788
789        self.station['configured'] = True
790        self.station['type'] = 'supplicant'
791        self.station['interface'] = interface
792