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