• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import collections
6import copy
7import logging
8import random
9import string
10import tempfile
11import time
12
13from autotest_lib.client.common_lib import error
14from autotest_lib.client.common_lib import utils
15from autotest_lib.client.common_lib.cros import path_utils
16from autotest_lib.client.common_lib.cros.network import interface
17from autotest_lib.client.common_lib.cros.network import netblock
18from autotest_lib.client.common_lib.cros.network import ping_runner
19from autotest_lib.server import hosts
20from autotest_lib.server import site_linux_system
21from autotest_lib.server.cros import dnsname_mangler
22from autotest_lib.server.cros.network import hostap_config
23
24
25StationInstance = collections.namedtuple('StationInstance',
26                                         ['ssid', 'interface', 'dev_type'])
27HostapdInstance = collections.namedtuple('HostapdInstance',
28                                         ['ssid', 'conf_file', 'log_file',
29                                          'interface', 'config_dict',
30                                          'stderr_log_file',
31                                          'scenario_name'])
32
33# Send magic packets here, so they can wake up the system but are otherwise
34# dropped.
35UDP_DISCARD_PORT = 9
36
37def build_router_hostname(client_hostname=None, router_hostname=None):
38    """Build a router hostname from a client hostname.
39
40    @param client_hostname: string hostname of DUT connected to a router.
41    @param router_hostname: string hostname of router.
42    @return string hostname of connected router or None if the hostname
43            cannot be inferred from the client hostname.
44
45    """
46    if not router_hostname and not client_hostname:
47        raise error.TestError('Either client_hostname or router_hostname must '
48                              'be specified to build_router_hostname.')
49
50    return dnsname_mangler.get_router_addr(client_hostname,
51                                           cmdline_override=router_hostname)
52
53
54def build_router_proxy(test_name='', client_hostname=None, router_addr=None,
55                       enable_avahi=False):
56    """Build up a LinuxRouter object.
57
58    Verifies that the remote host responds to ping.
59    Either client_hostname or router_addr must be specified.
60
61    @param test_name: string name of this test (e.g. 'network_WiFi_TestName').
62    @param client_hostname: string hostname of DUT if we're in the lab.
63    @param router_addr: string DNS/IPv4 address to use for router host object.
64    @param enable_avahi: boolean True iff avahi should be started on the router.
65
66    @return LinuxRouter or raise error.TestError on failure.
67
68    """
69    router_hostname = build_router_hostname(client_hostname=client_hostname,
70                                            router_hostname=router_addr)
71    logging.info('Connecting to router at %s', router_hostname)
72    ping_helper = ping_runner.PingRunner()
73    if not ping_helper.simple_ping(router_hostname):
74        raise error.TestError('Router at %s is not pingable.' %
75                              router_hostname)
76
77    # Use CrosHost for all router hosts and avoid host detection.
78    # Host detection would use JetstreamHost for Whirlwind routers.
79    # JetstreamHost assumes ap-daemons are running.
80    # Testbed routers run the testbed-ap profile with no ap-daemons.
81    # TODO(ecgh): crbug.com/757075 Fix testbed-ap JetstreamHost detection.
82    return LinuxRouter(hosts.create_host(router_hostname,
83                                         host_class=hosts.CrosHost),
84                       test_name,
85                       enable_avahi=enable_avahi)
86
87
88class LinuxRouter(site_linux_system.LinuxSystem):
89    """Linux/mac80211-style WiFi Router support for WiFiTest class.
90
91    This class implements test methods/steps that communicate with a
92    router implemented with Linux/mac80211.  The router must
93    be pre-configured to enable ssh access and have a mac80211-based
94    wireless device.  We also assume hostapd 0.7.x and iw are present
95    and any necessary modules are pre-loaded.
96
97    """
98
99    KNOWN_TEST_PREFIX = 'network_WiFi_'
100    POLLING_INTERVAL_SECONDS = 0.5
101    STARTUP_TIMEOUT_SECONDS = 30
102    SUFFIX_LETTERS = string.ascii_lowercase + string.digits
103    SUBNET_PREFIX_OCTETS = (192, 168)
104
105    HOSTAPD_CONF_FILE_PATTERN = '/tmp/hostapd-test-%s.conf'
106    HOSTAPD_LOG_FILE_PATTERN = '/tmp/hostapd-test-%s.log'
107    HOSTAPD_STDERR_LOG_FILE_PATTERN = '/tmp/hostapd-stderr-test-%s.log'
108    HOSTAPD_CONTROL_INTERFACE_PATTERN = '/tmp/hostapd-test-%s.ctrl'
109    HOSTAPD_DRIVER_NAME = 'nl80211'
110
111    STATION_CONF_FILE_PATTERN = '/tmp/wpa-supplicant-test-%s.conf'
112    STATION_LOG_FILE_PATTERN = '/tmp/wpa-supplicant-test-%s.log'
113    STATION_PID_FILE_PATTERN = '/tmp/wpa-supplicant-test-%s.pid'
114
115    MGMT_FRAME_SENDER_LOG_FILE = '/tmp/send_management_frame-test.log'
116
117    PROBE_RESPONSE_FOOTER_FILE = '/tmp/autotest-probe_response_footer'
118
119    _RNG_AVAILABLE = '/sys/class/misc/hw_random/rng_available'
120    _RNG_CURRENT = '/sys/class/misc/hw_random/rng_current'
121
122    def get_capabilities(self):
123        """@return iterable object of AP capabilities for this system."""
124        caps = set()
125        try:
126            self.cmd_send_management_frame = path_utils.must_be_installed(
127                    '/usr/bin/send_management_frame', host=self.host)
128            caps.add(self.CAPABILITY_SEND_MANAGEMENT_FRAME)
129        except error.TestFail:
130            pass
131        return super(LinuxRouter, self).get_capabilities().union(caps)
132
133
134    @property
135    def router(self):
136        """Deprecated.  Use self.host instead.
137
138        @return Host object representing the remote router.
139
140        """
141        return self.host
142
143
144    @property
145    def wifi_ip(self):
146        """Simple accessor for the WiFi IP when there is only one AP.
147
148        @return string IP of WiFi interface.
149
150        """
151        if len(self.local_servers) != 1:
152            raise error.TestError('Could not pick a WiFi IP to return.')
153
154        return self.get_wifi_ip(0)
155
156
157    def __init__(self, host, test_name, enable_avahi=False):
158        """Build a LinuxRouter.
159
160        @param host Host object representing the remote machine.
161        @param test_name string name of this test.  Used in SSID creation.
162        @param enable_avahi: boolean True iff avahi should be started on the
163                router.
164
165        """
166        super(LinuxRouter, self).__init__(host, 'router')
167        self._ssid_prefix = test_name
168        self._enable_avahi = enable_avahi
169        self.__setup()
170
171
172    def __setup(self):
173        """Set up this system.
174
175        Can be used either to complete initialization of a LinuxRouter
176        object, or to re-establish a good state after a reboot.
177
178        """
179        self.cmd_dhcpd = '/usr/sbin/dhcpd'
180        self.cmd_hostapd = path_utils.must_be_installed(
181                '/usr/sbin/hostapd', host=self.host)
182        self.cmd_hostapd_cli = path_utils.must_be_installed(
183                '/usr/sbin/hostapd_cli', host=self.host)
184        self.cmd_wpa_supplicant = path_utils.must_be_installed(
185                '/usr/sbin/wpa_supplicant', host=self.host)
186        self.dhcpd_conf = '/tmp/dhcpd.%s.conf'
187        self.dhcpd_leases = '/tmp/dhcpd.leases'
188
189        # TODO(crbug.com/839164): some routers fill their stateful partition
190        # with uncollected metrics.
191        self.host.run('rm -f /var/lib/metrics/uma-events', ignore_status=True)
192
193        # Log the most recent message on the router so that we can rebuild the
194        # suffix relevant to us when debugging failures.
195        last_log_line = self.host.run('tail -1 /var/log/messages',
196                                      ignore_status=True).stdout
197        # We're trying to get the timestamp from:
198        # 2014-07-23T17:29:34.961056+00:00 localhost kernel: blah blah blah
199        self._log_start_timestamp = last_log_line.strip().partition(' ')[0]
200        if self._log_start_timestamp:
201            logging.debug('Will only retrieve logs after %s.',
202                          self._log_start_timestamp)
203        else:
204            # If syslog is empty, we just use a wildcard pattern, to grab
205            # everything.
206            logging.debug('Empty or corrupt log; will retrieve whole log')
207            self._log_start_timestamp = '.'
208
209        # hostapd configuration persists throughout the test, subsequent
210        # 'config' commands only modify it.
211        if self._ssid_prefix.startswith(self.KNOWN_TEST_PREFIX):
212            # Many of our tests start with an uninteresting prefix.
213            # Remove it so we can have more unique bytes.
214            self._ssid_prefix = self._ssid_prefix[len(self.KNOWN_TEST_PREFIX):]
215        self._number_unique_ssids = 0
216
217        self._total_hostapd_instances = 0
218        self.local_servers = []
219        self.server_address_index = []
220        self.hostapd_instances = []
221        self.station_instances = []
222        self.dhcp_low = 1
223        self.dhcp_high = 128
224
225        # Tear down hostapbr bridge interfaces
226        result = self.host.run('ls -d /sys/class/net/%s*' %
227                               self.HOSTAP_BRIDGE_INTERFACE_PREFIX,
228                               ignore_status=True)
229        if result.exit_status == 0:
230            for path in result.stdout.splitlines():
231                self.delete_link(path.split('/')[-1])
232
233        # Kill hostapd and dhcp server if already running.
234        self._kill_process_instance('hostapd', timeout_seconds=30)
235        self.stop_dhcp_server(instance=None)
236
237        # Place us in the US by default
238        self.iw_runner.set_regulatory_domain('US')
239
240        self.enable_all_antennas()
241
242        # Some tests want this functionality, but otherwise, it's a distraction.
243        if self._enable_avahi:
244            self.host.run('start avahi', ignore_status=True)
245        else:
246            self.host.run('stop avahi', ignore_status=True)
247
248        # Some routers have bad (slow?) random number generators.
249        self.rng_configure()
250
251
252    def close(self):
253        """Close global resources held by this system."""
254        self.deconfig()
255        # dnsmasq and hostapd cause interesting events to go to system logs.
256        # Retrieve only the suffix of the logs after the timestamp we stored on
257        # router creation.
258        self.host.run("sed -n -e '/%s/,$p' /var/log/messages >/tmp/router_log" %
259                      self._log_start_timestamp, ignore_status=True)
260        self.host.get_file('/tmp/router_log', 'debug/router_host_messages')
261        super(LinuxRouter, self).close()
262
263
264    def reboot(self, timeout):
265        """Reboot this router, and restore it to a known-good state.
266
267        @param timeout Maximum seconds to wait for router to return.
268
269        """
270        super(LinuxRouter, self).reboot(timeout)
271        self.__setup()
272
273
274    def has_local_server(self):
275        """@return True iff this router has local servers configured."""
276        return bool(self.local_servers)
277
278
279    def start_hostapd(self, configuration):
280        """Start a hostapd instance described by conf.
281
282        @param configuration HostapConfig object.
283
284        """
285        # Figure out the correct interface.
286        interface = self.get_wlanif(configuration.frequency, 'managed',
287                                    configuration.min_streams)
288        phy_name = self.iw_runner.get_interface(interface).phy
289
290        conf_file = self.HOSTAPD_CONF_FILE_PATTERN % interface
291        log_file = self.HOSTAPD_LOG_FILE_PATTERN % interface
292        stderr_log_file = self.HOSTAPD_STDERR_LOG_FILE_PATTERN % interface
293        control_interface = self.HOSTAPD_CONTROL_INTERFACE_PATTERN % interface
294        hostapd_conf_dict = configuration.generate_dict(
295                interface, control_interface,
296                self.build_unique_ssid(suffix=configuration.ssid_suffix))
297        logging.debug('hostapd parameters: %r', hostapd_conf_dict)
298
299        # Generate hostapd.conf.
300        self.router.run("cat <<EOF >%s\n%s\nEOF\n" %
301            (conf_file, '\n'.join(
302            "%s=%s" % kv for kv in hostapd_conf_dict.iteritems())))
303
304        # Run hostapd.
305        logging.info('Starting hostapd on %s(%s) channel=%s...',
306                     interface, phy_name, configuration.channel)
307        self.router.run('rm %s' % log_file, ignore_status=True)
308        self.router.run('stop wpasupplicant', ignore_status=True)
309        start_command = '%s -dd -t %s > %s 2> %s & echo $!' % (
310                self.cmd_hostapd, conf_file, log_file, stderr_log_file)
311        pid = int(self.router.run(start_command).stdout.strip())
312        self.hostapd_instances.append(HostapdInstance(
313                hostapd_conf_dict['ssid'],
314                conf_file,
315                log_file,
316                interface,
317                hostapd_conf_dict.copy(),
318                stderr_log_file,
319                configuration.scenario_name))
320
321        # Wait for confirmation that the router came up.
322        logging.info('Waiting for hostapd to startup.')
323        utils.poll_for_condition(
324                condition=lambda: self._has_hostapd_started(log_file, pid),
325                exception=error.TestFail('Timed out while waiting for hostapd '
326                                         'to start.'),
327                timeout=self.STARTUP_TIMEOUT_SECONDS,
328                sleep_interval=self.POLLING_INTERVAL_SECONDS)
329
330        if configuration.frag_threshold:
331            threshold = self.iw_runner.get_fragmentation_threshold(phy_name)
332            if threshold != configuration.frag_threshold:
333                raise error.TestNAError('Router does not support setting '
334                                        'fragmentation threshold')
335
336
337    def _has_hostapd_started(self, log_file, pid):
338        """Determines if hostapd has started.
339
340        @return Whether or not hostapd has started.
341        @raise error.TestFail if there was a bad config or hostapd terminated.
342        """
343        success = self.router.run(
344            'grep "Setup of interface done" %s' % log_file,
345            ignore_status=True).exit_status == 0
346        if success:
347            return True
348
349        # A common failure is an invalid router configuration.
350        # Detect this and exit early if we see it.
351        bad_config = self.router.run(
352            'grep "Interface initialization failed" %s' % log_file,
353            ignore_status=True).exit_status == 0
354        if bad_config:
355            raise error.TestFail('hostapd failed to initialize AP '
356                                 'interface.')
357
358        if pid:
359            early_exit = self.router.run('kill -0 %d' % pid,
360                                         ignore_status=True).exit_status
361            if early_exit:
362                raise error.TestFail('hostapd process terminated.')
363
364        return False
365
366
367    def _kill_process_instance(self,
368                               process,
369                               instance=None,
370                               timeout_seconds=10,
371                               ignore_timeouts=False):
372        """Kill a process on the router.
373
374        Kills remote program named |process| (optionally only a specific
375        |instance|).  Wait |timeout_seconds| for |process| to die
376        before returning.  If |ignore_timeouts| is False, raise
377        a TestError on timeouts.
378
379        @param process: string name of process to kill.
380        @param instance: string fragment of the command line unique to
381                this instance of the remote process.
382        @param timeout_seconds: float timeout in seconds to wait.
383        @param ignore_timeouts: True iff we should ignore failures to
384                kill processes.
385        @return True iff the specified process has exited.
386
387        """
388        if instance is not None:
389            search_arg = '-f "^%s.*%s"' % (process, instance)
390        else:
391            search_arg = process
392
393        self.host.run('pkill %s' % search_arg, ignore_status=True)
394
395        # Wait for process to die
396        time.sleep(self.POLLING_INTERVAL_SECONDS)
397        try:
398            utils.poll_for_condition(
399                    condition=lambda: self.host.run(
400                            'pgrep -l %s' % search_arg,
401                            ignore_status=True).exit_status != 0,
402                    timeout=timeout_seconds,
403                    sleep_interval=self.POLLING_INTERVAL_SECONDS)
404        except utils.TimeoutError:
405            if ignore_timeouts:
406                return False
407
408            raise error.TestError(
409                'Timed out waiting for %s%s to die' %
410                (process,
411                '' if instance is None else ' (instance=%s)' % instance))
412        return True
413
414
415    def kill_hostapd_instance(self, instance):
416        """Kills a hostapd instance.
417
418        @param instance HostapdInstance object.
419
420        """
421        is_dead = self._kill_process_instance(
422                self.cmd_hostapd,
423                instance=instance.conf_file,
424                timeout_seconds=30,
425                ignore_timeouts=True)
426        if instance.scenario_name:
427            log_identifier = instance.scenario_name
428        else:
429            log_identifier = '%d_%s' % (
430                self._total_hostapd_instances, instance.interface)
431        files_to_copy = [(instance.log_file,
432                          'debug/hostapd_router_%s.log' % log_identifier),
433                         (instance.stderr_log_file,
434                          'debug/hostapd_router_%s.stderr.log' %
435                          log_identifier)]
436        for remote_file, local_file in files_to_copy:
437            if self.host.run('ls %s >/dev/null 2>&1' % remote_file,
438                             ignore_status=True).exit_status:
439                logging.error('Did not collect hostapd log file because '
440                              'it was missing.')
441            else:
442                self.router.get_file(remote_file, local_file)
443        self._total_hostapd_instances += 1
444        if not is_dead:
445            raise error.TestError('Timed out killing hostapd.')
446
447
448    def build_unique_ssid(self, suffix=''):
449        """ Build our unique token by base-<len(self.SUFFIX_LETTERS)> encoding
450        the number of APs we've constructed already.
451
452        @param suffix string to append to SSID
453
454        """
455        base = len(self.SUFFIX_LETTERS)
456        number = self._number_unique_ssids
457        self._number_unique_ssids += 1
458        unique = ''
459        while number or not unique:
460            unique = self.SUFFIX_LETTERS[number % base] + unique
461            number = number / base
462        # And salt the SSID so that tests running in adjacent cells are unlikely
463        # to pick the same SSID and we're resistent to beacons leaking out of
464        # cells.
465        salt = ''.join([random.choice(self.SUFFIX_LETTERS) for x in range(5)])
466        return '_'.join([self._ssid_prefix, unique, salt, suffix])[-32:]
467
468
469    def rng_configure(self):
470        """Configure the random generator to our liking.
471
472        Some routers (particularly, Gale) seem to have bad Random Number
473        Generators, such that hostapd can't always generate keys fast enough.
474        The on-board TPM seems to serve as a better generator, so we try to
475        switch to that if available.
476
477        Symptoms of a slow RNG: hostapd complains with:
478
479          WPA: Not enough entropy in random pool to proceed - reject first
480          4-way handshake
481
482        Ref:
483        https://chromium.googlesource.com/chromiumos/third_party/hostap/+/7ea51f728bb7/src/ap/wpa_auth.c#1854
484
485        Linux devices may have RNG parameters at
486        /sys/class/misc/hw_random/rng_{available,current}. See:
487
488          https://www.kernel.org/doc/Documentation/hw_random.txt
489
490        """
491
492        available = self.host.run('cat %s' % self._RNG_AVAILABLE, \
493                                  ignore_status=True).stdout.strip().split(' ')
494        # System may not have HWRNG support. Just skip this.
495        if available == "":
496            return
497        current = self.host.run('cat %s' % self._RNG_CURRENT).stdout. \
498                                strip()
499        want_rng = "tpm-rng"
500
501        logging.debug("Available / current RNGs on router: %r / %s",
502                      available, current)
503        if want_rng in available and want_rng != current:
504            logging.debug("Switching RNGs: %s -> %s", current, want_rng)
505            self.host.run('echo -n "%s" > %s' % (want_rng, self._RNG_CURRENT))
506
507
508    def hostap_configure(self, configuration, multi_interface=None):
509        """Build up a hostapd configuration file and start hostapd.
510
511        Also setup a local server if this router supports them.
512
513        @param configuration HosetapConfig object.
514        @param multi_interface bool True iff multiple interfaces allowed.
515
516        """
517        if multi_interface is None and (self.hostapd_instances or
518                                        self.station_instances):
519            self.deconfig()
520        if configuration.is_11ac:
521            router_caps = self.get_capabilities()
522            if site_linux_system.LinuxSystem.CAPABILITY_VHT not in router_caps:
523                raise error.TestNAError('Router does not have AC support')
524
525        if configuration.use_bridge:
526            configuration._bridge = self.get_brif()
527
528        self.start_hostapd(configuration)
529        interface = self.hostapd_instances[-1].interface
530        self.iw_runner.set_tx_power(interface, 'auto')
531        self.set_beacon_footer(interface, configuration.beacon_footer)
532        self.start_local_server(interface, bridge=configuration.bridge)
533        logging.info('AP configured.')
534
535
536    def ibss_configure(self, config):
537        """Configure a station based AP in IBSS mode.
538
539        Extract relevant configuration objects from |config| despite not
540        actually being a hostap managed endpoint.
541
542        @param config HostapConfig object.
543
544        """
545        if self.station_instances or self.hostapd_instances:
546            self.deconfig()
547        interface = self.get_wlanif(config.frequency, 'ibss')
548        ssid = (config.ssid or
549                self.build_unique_ssid(suffix=config.ssid_suffix))
550        # Connect the station
551        self.router.run('%s link set %s up' % (self.cmd_ip, interface))
552        self.iw_runner.ibss_join(interface, ssid, config.frequency)
553        # Always start a local server.
554        self.start_local_server(interface)
555        # Remember that this interface is up.
556        self.station_instances.append(
557                StationInstance(ssid=ssid, interface=interface,
558                                dev_type='ibss'))
559
560
561    def local_server_address(self, index):
562        """Get the local server address for an interface.
563
564        When we multiple local servers, we give them static IP addresses
565        like 192.168.*.254.
566
567        @param index int describing which local server this is for.
568
569        """
570        return '%d.%d.%d.%d' % (self.SUBNET_PREFIX_OCTETS + (index, 254))
571
572
573    def local_peer_ip_address(self, index):
574        """Get the IP address allocated for the peer associated to the AP.
575
576        This address is assigned to a locally associated peer device that
577        is created for the DUT to perform connectivity tests with.
578        When we have multiple local servers, we give them static IP addresses
579        like 192.168.*.253.
580
581        @param index int describing which local server this is for.
582
583        """
584        return '%d.%d.%d.%d' % (self.SUBNET_PREFIX_OCTETS + (index, 253))
585
586    def local_bridge_address(self, index):
587        """Get the bridge address for an interface.
588
589        This address is assigned to a local bridge device.
590
591        @param index int describing which local server this is for.
592
593        """
594        return '%d.%d.%d.%d' % (self.SUBNET_PREFIX_OCTETS + (index, 252))
595
596    def local_peer_mac_address(self):
597        """Get the MAC address of the peer interface.
598
599        @return string MAC address of the peer interface.
600
601        """
602        iface = interface.Interface(self.station_instances[0].interface,
603                                    self.router)
604        return iface.mac_address
605
606
607    def _get_unused_server_address_index(self):
608        """@return an unused server address index."""
609        for address_index in range(0, 256):
610            if address_index not in self.server_address_index:
611                return address_index
612        raise error.TestFail('No available server address index')
613
614
615    def change_server_address_index(self, ap_num=0, server_address_index=None):
616        """Restart the local server with a different server address index.
617
618        This will restart the local server with different gateway IP address
619        and DHCP address ranges.
620
621        @param ap_num: int hostapd instance number.
622        @param server_address_index: int server address index.
623
624        """
625        interface = self.local_servers[ap_num]['interface'];
626        # Get an unused server address index if one is not specified, which
627        # will be different from the one that's currently in used.
628        if server_address_index is None:
629            server_address_index = self._get_unused_server_address_index()
630
631        # Restart local server with the new server address index.
632        self.stop_local_server(self.local_servers[ap_num])
633        self.start_local_server(interface,
634                                ap_num=ap_num,
635                                server_address_index=server_address_index)
636
637
638    def start_local_server(self,
639                           interface,
640                           ap_num=None,
641                           server_address_index=None,
642                           bridge=None):
643        """Start a local server on an interface.
644
645        @param interface string (e.g. wlan0)
646        @param ap_num int the ap instance to start the server for
647        @param server_address_index int server address index
648        @param bridge string (e.g. br0)
649
650        """
651        logging.info('Starting up local server...')
652
653        if len(self.local_servers) >= 256:
654            raise error.TestFail('Exhausted available local servers')
655
656        # Get an unused server address index if one is not specified.
657        # Validate server address index if one is specified.
658        if server_address_index is None:
659            server_address_index = self._get_unused_server_address_index()
660        elif server_address_index in self.server_address_index:
661            raise error.TestFail('Server address index %d already in used' %
662                                 server_address_index)
663
664        server_addr = netblock.from_addr(
665                self.local_server_address(server_address_index),
666                prefix_len=24)
667
668        params = {}
669        params['address_index'] = server_address_index
670        params['netblock'] = server_addr
671        params['dhcp_range'] = ' '.join(
672            (server_addr.get_addr_in_block(1),
673             server_addr.get_addr_in_block(128)))
674        params['interface'] = interface
675        params['bridge'] = bridge
676        params['ip_params'] = ('%s broadcast %s dev %s' %
677                               (server_addr.netblock,
678                                server_addr.broadcast,
679                                interface))
680        if ap_num is None:
681            self.local_servers.append(params)
682        else:
683            self.local_servers.insert(ap_num, params)
684        self.server_address_index.append(server_address_index)
685
686        self.router.run('%s addr flush %s' %
687                        (self.cmd_ip, interface))
688        self.router.run('%s addr add %s' %
689                        (self.cmd_ip, params['ip_params']))
690        self.router.run('%s link set %s up' %
691                        (self.cmd_ip, interface))
692        if params['bridge']:
693            bridge_addr = netblock.from_addr(
694                    self.local_bridge_address(server_address_index),
695                    prefix_len=24)
696            self.router.run("ifconfig %s %s" %
697                           (params['bridge'], bridge_addr.netblock))
698        self.start_dhcp_server(interface)
699
700
701    def stop_local_server(self, server):
702        """Stop a local server on the router
703
704        @param server object server configuration parameters.
705
706        """
707        self.stop_dhcp_server(server['interface'])
708        self.router.run("%s addr del %s" %
709                        (self.cmd_ip, server['ip_params']),
710                        ignore_status=True)
711        self.server_address_index.remove(server['address_index'])
712        self.local_servers.remove(server)
713
714
715    def start_dhcp_server(self, interface):
716        """Start a dhcp server on an interface.
717
718        @param interface string (e.g. wlan0)
719
720        """
721        for server in self.local_servers:
722            if server['interface'] == interface:
723                params = server
724                break
725        else:
726            raise error.TestFail('Could not find local server '
727                                 'to match interface: %r' % interface)
728        server_addr = params['netblock']
729        dhcpd_conf_file = self.dhcpd_conf % interface
730        dhcp_conf = '\n'.join([
731            'port=0',  # disables DNS server
732            'bind-interfaces',
733            'log-dhcp',
734            'dhcp-range=%s' % ','.join((server_addr.get_addr_in_block(1),
735                                        server_addr.get_addr_in_block(128))),
736            'interface=%s' % (params['bridge'] or params['interface']),
737            'dhcp-leasefile=%s' % self.dhcpd_leases])
738        self.router.run('cat <<EOF >%s\n%s\nEOF\n' %
739            (dhcpd_conf_file, dhcp_conf))
740        self.router.run('dnsmasq --conf-file=%s' % dhcpd_conf_file)
741
742
743    def stop_dhcp_server(self, instance=None):
744        """Stop a dhcp server on the router.
745
746        @param instance string instance to kill.
747
748        """
749        self._kill_process_instance('dnsmasq', instance=instance)
750
751
752    def get_wifi_channel(self, ap_num):
753        """Return channel of BSS corresponding to |ap_num|.
754
755        @param ap_num int which BSS to get the channel of.
756        @return int primary channel of BSS.
757
758        """
759        instance = self.hostapd_instances[ap_num]
760        return instance.config_dict['channel']
761
762
763    def get_wifi_ip(self, ap_num):
764        """Return IP address on the WiFi subnet of a local server on the router.
765
766        If no local servers are configured (e.g. for an RSPro), a TestFail will
767        be raised.
768
769        @param ap_num int which local server to get an address from.
770
771        """
772        if not self.local_servers:
773            raise error.TestError('No IP address assigned')
774
775        return self.local_servers[ap_num]['netblock'].addr
776
777
778    def get_wifi_ip_subnet(self, ap_num):
779        """Return subnet of WiFi AP instance.
780
781        If no APs are configured a TestError will be raised.
782
783        @param ap_num int which local server to get an address from.
784
785        """
786        if not self.local_servers:
787            raise error.TestError('No APs configured.')
788
789        return self.local_servers[ap_num]['netblock'].subnet
790
791
792    def get_hostapd_interface(self, ap_num):
793        """Get the name of the interface associated with a hostapd instance.
794
795        @param ap_num: int hostapd instance number.
796        @return string interface name (e.g. 'managed0').
797
798        """
799        if ap_num not in range(len(self.hostapd_instances)):
800            raise error.TestFail('Invalid instance number (%d) with %d '
801                                 'instances configured.' %
802                                 (ap_num, len(self.hostapd_instances)))
803
804        instance = self.hostapd_instances[ap_num]
805        return instance.interface
806
807
808    def get_station_interface(self, instance):
809        """Get the name of the interface associated with a station.
810
811        @param instance: int station instance number.
812        @return string interface name (e.g. 'managed0').
813
814        """
815        if instance not in range(len(self.station_instances)):
816            raise error.TestFail('Invalid instance number (%d) with %d '
817                                 'instances configured.' %
818                                 (instance, len(self.station_instances)))
819
820        instance = self.station_instances[instance]
821        return instance.interface
822
823
824    def get_hostapd_mac(self, ap_num):
825        """Return the MAC address of an AP in the test.
826
827        @param ap_num int index of local server to read the MAC address from.
828        @return string MAC address like 00:11:22:33:44:55.
829
830        """
831        interface_name = self.get_hostapd_interface(ap_num)
832        ap_interface = interface.Interface(interface_name, self.host)
833        return ap_interface.mac_address
834
835
836    def get_hostapd_phy(self, ap_num):
837        """Get name of phy for hostapd instance.
838
839        @param ap_num int index of hostapd instance.
840        @return string phy name of phy corresponding to hostapd's
841                managed interface.
842
843        """
844        interface = self.iw_runner.get_interface(
845                self.get_hostapd_interface(ap_num))
846        return interface.phy
847
848
849    def deconfig(self):
850        """A legacy, deprecated alias for deconfig_aps."""
851        self.deconfig_aps()
852
853
854    def deconfig_aps(self, instance=None, silent=False):
855        """De-configure an AP (will also bring wlan down).
856
857        @param instance: int or None.  If instance is None, will bring down all
858                instances of hostapd.
859        @param silent: True if instances should be brought without de-authing
860                the DUT.
861
862        """
863        if not self.hostapd_instances and not self.station_instances:
864            return
865
866        if self.hostapd_instances:
867            local_servers = []
868            if instance is not None:
869                instances = [ self.hostapd_instances.pop(instance) ]
870                for server in self.local_servers:
871                    if server['interface'] == instances[0].interface:
872                        local_servers = [server]
873                        break
874            else:
875                instances = self.hostapd_instances
876                self.hostapd_instances = []
877                local_servers = copy.copy(self.local_servers)
878
879            for instance in instances:
880                if silent:
881                    # Deconfigure without notifying DUT.  Remove the interface
882                    # hostapd uses to send beacon and DEAUTH packets.
883                    self.remove_interface(instance.interface)
884
885                self.kill_hostapd_instance(instance)
886                self.release_interface(instance.interface)
887        if self.station_instances:
888            local_servers = copy.copy(self.local_servers)
889            instance = self.station_instances.pop()
890            if instance.dev_type == 'ibss':
891                self.iw_runner.ibss_leave(instance.interface)
892            elif instance.dev_type == 'managed':
893                self._kill_process_instance(self.cmd_wpa_supplicant,
894                                            instance=instance.interface)
895            else:
896                self.iw_runner.disconnect_station(instance.interface)
897            self.router.run('%s link set %s down' %
898                            (self.cmd_ip, instance.interface))
899
900        for server in local_servers:
901            self.stop_local_server(server)
902
903        for brif in range(self._brif_index):
904            self.delete_link('%s%d' %
905                             (self.HOSTAP_BRIDGE_INTERFACE_PREFIX, brif))
906
907
908    def delete_link(self, name):
909        """Delete link using the `ip` command.
910
911        @param name string link name.
912
913        """
914        self.host.run('%s link del %s' % (self.cmd_ip, name),
915                      ignore_status=True)
916
917
918    def set_ap_interface_down(self, instance=0):
919        """Bring down the hostapd interface.
920
921        @param instance int router instance number.
922
923        """
924        self.host.run('%s link set %s down' %
925                      (self.cmd_ip, self.get_hostapd_interface(instance)))
926
927
928    def confirm_pmksa_cache_use(self, instance=0):
929        """Verify that the PMKSA auth was cached on a hostapd instance.
930
931        @param instance int router instance number.
932
933        """
934        log_file = self.hostapd_instances[instance].log_file
935        pmksa_match = 'PMK from PMKSA cache'
936        result = self.router.run('grep -q "%s" %s' % (pmksa_match, log_file),
937                                 ignore_status=True)
938        if result.exit_status:
939            raise error.TestFail('PMKSA cache was not used in roaming.')
940
941
942    def get_ssid(self, instance=None):
943        """@return string ssid for the network stemming from this router."""
944        if instance is None:
945            instance = 0
946            if len(self.hostapd_instances) > 1:
947                raise error.TestFail('No instance of hostapd specified with '
948                                     'multiple instances present.')
949
950        if self.hostapd_instances:
951            return self.hostapd_instances[instance].ssid
952
953        if self.station_instances:
954            return self.station_instances[0].ssid
955
956        raise error.TestFail('Requested ssid of an unconfigured AP.')
957
958
959    def deauth_client(self, client_mac):
960        """Deauthenticates a client described in params.
961
962        @param client_mac string containing the mac address of the client to be
963               deauthenticated.
964
965        """
966        control_if = self.hostapd_instances[-1].config_dict['ctrl_interface']
967        self.router.run('%s -p%s deauthenticate %s' %
968                        (self.cmd_hostapd_cli, control_if, client_mac))
969
970    def send_bss_tm_req(self, client_mac, neighbor_list):
971        """Send a BSS Transition Management Request to a client.
972
973        @param client_mac string containing the mac address of the client.
974        @param neighbor_list list of strings containing mac addresses of
975               candidate APs.
976        @return bool True if BSS_TM_REQ is sent successfully.
977
978        """
979        control_if = self.hostapd_instances[0].config_dict['ctrl_interface']
980        command = ('%s -p%s BSS_TM_REQ %s neighbor=%s,0,0,0,0 pref=1' %
981                   (self.cmd_hostapd_cli, control_if, client_mac,
982                    ',0,0,0,0 neighbor='.join(neighbor_list)))
983        ret = self.router.run(command).stdout
984        if ret.splitlines()[-1] != 'OK':
985            return False
986        return True
987
988    def _prep_probe_response_footer(self, footer):
989        """Write probe response footer temporarily to a local file and copy
990        over to test router.
991
992        @param footer string containing bytes for the probe response footer.
993        @raises AutoservRunError: If footer file copy fails.
994
995        """
996        with tempfile.NamedTemporaryFile() as fp:
997            fp.write(footer)
998            fp.flush()
999            try:
1000                self.host.send_file(fp.name, self.PROBE_RESPONSE_FOOTER_FILE)
1001            except error.AutoservRunError:
1002                logging.error('failed to copy footer file to AP')
1003                raise
1004
1005
1006    def send_management_frame_on_ap(self, frame_type, channel, instance=0):
1007        """Injects a management frame into an active hostapd session.
1008
1009        @param frame_type string the type of frame to send.
1010        @param channel int targeted channel
1011        @param instance int indicating which hostapd instance to inject into.
1012
1013        """
1014        hostap_interface = self.hostapd_instances[instance].interface
1015        interface = self.get_wlanif(0, 'monitor', same_phy_as=hostap_interface)
1016        self.router.run("%s link set %s up" % (self.cmd_ip, interface))
1017        self.router.run('%s -i %s -t %s -c %d' %
1018                        (self.cmd_send_management_frame, interface, frame_type,
1019                         channel))
1020        self.release_interface(interface)
1021
1022
1023    def send_management_frame(self, interface, frame_type, channel,
1024                              ssid_prefix=None, num_bss=None,
1025                              frame_count=None, delay=None,
1026                              dest_addr=None, probe_resp_footer=None):
1027        """
1028        Injects management frames on specify channel |frequency|.
1029
1030        This function will spawn off a new process to inject specified
1031        management frames |frame_type| at the specified interface |interface|.
1032
1033        @param interface string interface to inject frames.
1034        @param frame_type string message type.
1035        @param channel int targeted channel.
1036        @param ssid_prefix string SSID prefix.
1037        @param num_bss int number of BSS.
1038        @param frame_count int number of frames to send.
1039        @param delay int milliseconds delay between frames.
1040        @param dest_addr string destination address (DA) MAC address.
1041        @param probe_resp_footer string footer for probe response.
1042
1043        @return int PID of the newly created process.
1044
1045        """
1046        command = '%s -i %s -t %s -c %d' % (self.cmd_send_management_frame,
1047                                interface, frame_type, channel)
1048        if ssid_prefix is not None:
1049            command += ' -s %s' % (ssid_prefix)
1050        if num_bss is not None:
1051            command += ' -b %d' % (num_bss)
1052        if frame_count is not None:
1053            command += ' -n %d' % (frame_count)
1054        if delay is not None:
1055            command += ' -d %d' % (delay)
1056        if dest_addr is not None:
1057            command += ' -a %s' % (dest_addr)
1058        if probe_resp_footer is not None:
1059            self._prep_probe_response_footer(footer=probe_resp_footer)
1060            command += ' -f %s' % (self.PROBE_RESPONSE_FOOTER_FILE)
1061        command += ' > %s 2>&1 & echo $!' % (self.MGMT_FRAME_SENDER_LOG_FILE)
1062        pid = int(self.router.run(command).stdout)
1063        return pid
1064
1065
1066    def detect_client_deauth(self, client_mac, instance=0):
1067        """Detects whether hostapd has logged a deauthentication from
1068        |client_mac|.
1069
1070        @param client_mac string the MAC address of the client to detect.
1071        @param instance int indicating which hostapd instance to query.
1072
1073        """
1074        interface = self.hostapd_instances[instance].interface
1075        deauth_msg = "%s: deauthentication: STA=%s" % (interface, client_mac)
1076        log_file = self.hostapd_instances[instance].log_file
1077        result = self.router.run("grep -qi '%s' %s" % (deauth_msg, log_file),
1078                                 ignore_status=True)
1079        return result.exit_status == 0
1080
1081
1082    def detect_client_coexistence_report(self, client_mac, instance=0):
1083        """Detects whether hostapd has logged an action frame from
1084        |client_mac| indicating information about 20/40MHz BSS coexistence.
1085
1086        @param client_mac string the MAC address of the client to detect.
1087        @param instance int indicating which hostapd instance to query.
1088
1089        """
1090        coex_msg = ('nl80211: MLME event frame - hexdump(len=.*): '
1091                    '.. .. .. .. .. .. .. .. .. .. %s '
1092                    '.. .. .. .. .. .. .. .. 04 00.*48 01 ..' %
1093                    ' '.join(client_mac.split(':')))
1094        log_file = self.hostapd_instances[instance].log_file
1095        result = self.router.run("grep -qi '%s' %s" % (coex_msg, log_file),
1096                                 ignore_status=True)
1097        return result.exit_status == 0
1098
1099
1100    def add_connected_peer(self, instance=0):
1101        """Configure a station connected to a running AP instance.
1102
1103        Extract relevant configuration objects from the hostap
1104        configuration for |instance| and generate a wpa_supplicant
1105        instance that connects to it.  This allows the DUT to interact
1106        with a client entity that is also connected to the same AP.  A
1107        full wpa_supplicant instance is necessary here (instead of just
1108        using the "iw" command to connect) since we want to enable
1109        advanced features such as TDLS.
1110
1111        @param instance int indicating which hostapd instance to connect to.
1112
1113        """
1114        if not self.hostapd_instances:
1115            raise error.TestFail('Hostapd is not configured.')
1116
1117        if self.station_instances:
1118            raise error.TestFail('Station is already configured.')
1119
1120        ssid = self.get_ssid(instance)
1121        hostap_conf = self.hostapd_instances[instance].config_dict
1122        frequency = hostap_config.HostapConfig.get_frequency_for_channel(
1123                hostap_conf['channel'])
1124        self.configure_managed_station(
1125                ssid, frequency, self.local_peer_ip_address(instance))
1126        interface = self.station_instances[0].interface
1127        # Since we now have two network interfaces connected to the same
1128        # network, we need to disable the kernel's protection against
1129        # incoming packets to an "unexpected" interface.
1130        self.router.run('echo 2 > /proc/sys/net/ipv4/conf/%s/rp_filter' %
1131                        interface)
1132
1133        # Similarly, we'd like to prevent the hostap interface from
1134        # replying to ARP requests for the peer IP address and vice
1135        # versa.
1136        self.router.run('echo 1 > /proc/sys/net/ipv4/conf/%s/arp_ignore' %
1137                        interface)
1138        self.router.run('echo 1 > /proc/sys/net/ipv4/conf/%s/arp_ignore' %
1139                        hostap_conf['interface'])
1140
1141
1142    def configure_managed_station(self, ssid, frequency, ip_addr):
1143        """Configure a router interface to connect as a client to a network.
1144
1145        @param ssid: string SSID of network to join.
1146        @param frequency: int frequency required to join the network.
1147        @param ip_addr: IP address to assign to this interface
1148                        (e.g. '192.168.1.200').
1149
1150        """
1151        interface = self.get_wlanif(frequency, 'managed')
1152
1153        # TODO(pstew): Configure other bits like PSK, 802.11n if tests
1154        # require them...
1155        supplicant_config = (
1156                'network={\n'
1157                '  ssid="%(ssid)s"\n'
1158                '  key_mgmt=NONE\n'
1159                '}\n' % {'ssid': ssid}
1160        )
1161
1162        conf_file = self.STATION_CONF_FILE_PATTERN % interface
1163        log_file = self.STATION_LOG_FILE_PATTERN % interface
1164        pid_file = self.STATION_PID_FILE_PATTERN % interface
1165
1166        self.router.run('cat <<EOF >%s\n%s\nEOF\n' %
1167            (conf_file, supplicant_config))
1168
1169        # Connect the station.
1170        self.router.run('%s link set %s up' % (self.cmd_ip, interface))
1171        start_command = ('%s -dd -t -i%s -P%s -c%s -D%s >%s 2>&1 &' %
1172                         (self.cmd_wpa_supplicant,
1173                         interface, pid_file, conf_file,
1174                         self.HOSTAPD_DRIVER_NAME, log_file))
1175        self.router.run(start_command)
1176        self.iw_runner.wait_for_link(interface)
1177
1178        # Assign an IP address to this interface.
1179        self.router.run('%s addr add %s/24 dev %s' %
1180                        (self.cmd_ip, ip_addr, interface))
1181        self.station_instances.append(
1182                StationInstance(ssid=ssid, interface=interface,
1183                                dev_type='managed'))
1184
1185
1186    def send_magic_packet(self, dest_ip, dest_mac):
1187        """Sends a magic packet to the NIC with the given IP and MAC addresses.
1188
1189        @param dest_ip the IP address of the device to send the packet to
1190        @param dest_mac the hardware MAC address of the device
1191
1192        """
1193        # magic packet is 6 0xff bytes followed by the hardware address
1194        # 16 times
1195        mac_bytes = ''.join([chr(int(b, 16)) for b in dest_mac.split(':')])
1196        magic_packet = '\xff' * 6 + mac_bytes * 16
1197
1198        logging.info('Sending magic packet to %s...', dest_ip)
1199        self.host.run('python -uc "import socket, sys;'
1200                      's = socket.socket(socket.AF_INET, socket.SOCK_DGRAM);'
1201                      's.sendto(sys.stdin.read(), (\'%s\', %d))"' %
1202                      (dest_ip, UDP_DISCARD_PORT),
1203                      stdin=magic_packet)
1204
1205
1206    def set_beacon_footer(self, interface, footer=''):
1207        """Sets the beacon footer (appended IE information) for this interface.
1208
1209        @param interface string interface to set the footer on.
1210        @param footer string footer to be set on the interface.
1211
1212        """
1213        footer_file = ('/sys/kernel/debug/ieee80211/%s/beacon_footer' %
1214                       self.iw_runner.get_interface(interface).phy)
1215        if self.router.run('test -e %s' % footer_file,
1216                           ignore_status=True).exit_status != 0:
1217            logging.info('Beacon footer file does not exist.  Ignoring.')
1218            return
1219        self.host.run('echo -ne %s > %s' % ('%r' % footer, footer_file))
1220
1221
1222    def setup_bridge_mode_dhcp_server(self):
1223        """Setup an DHCP server for bridge mode.
1224
1225        Setup an DHCP server on the master interface of the virtual ethernet
1226        pair, with peer interface connected to the bridge interface. This is
1227        used for testing APs in bridge mode.
1228
1229        """
1230        # Start a local server on master interface of virtual ethernet pair.
1231        self.start_local_server(
1232                self.get_virtual_ethernet_master_interface())
1233        # Add peer interface to the bridge.
1234        self.add_interface_to_bridge(
1235                self.get_virtual_ethernet_peer_interface())
1236