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