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