1#!/usr/bin/env python3 2# 3# Copyright 2016 - Google, Inc. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17import collections 18import ipaddress 19import os 20import time 21from typing import FrozenSet, Set 22 23from acts import logger 24from acts import utils 25 26from acts.controllers import pdu 27from acts.controllers.ap_lib import ap_get_interface 28from acts.controllers.ap_lib import ap_iwconfig 29from acts.controllers.ap_lib import bridge_interface 30from acts.controllers.ap_lib import dhcp_config 31from acts.controllers.ap_lib import dhcp_server 32from acts.controllers.ap_lib import hostapd 33from acts.controllers.ap_lib import hostapd_ap_preset 34from acts.controllers.ap_lib import hostapd_constants 35from acts.controllers.ap_lib import hostapd_config 36from acts.controllers.ap_lib import radvd 37from acts.controllers.ap_lib import radvd_config 38from acts.controllers.ap_lib.extended_capabilities import ExtendedCapabilities 39from acts.controllers.ap_lib.wireless_network_management import BssTransitionManagementRequest 40from acts.controllers.utils_lib.commands import ip 41from acts.controllers.utils_lib.commands import route 42from acts.controllers.utils_lib.commands import shell 43from acts.controllers.utils_lib.ssh import connection 44from acts.controllers.utils_lib.ssh import settings 45from acts.libs.proc import job 46 47MOBLY_CONTROLLER_CONFIG_NAME = 'AccessPoint' 48ACTS_CONTROLLER_REFERENCE_NAME = 'access_points' 49_BRCTL = 'brctl' 50 51LIFETIME = 180 52PROC_NET_SNMP6 = '/proc/net/snmp6' 53SCAPY_INSTALL_COMMAND = 'sudo python setup.py install' 54RA_MULTICAST_ADDR = '33:33:00:00:00:01' 55RA_SCRIPT = 'sendra.py' 56 57 58def create(configs): 59 """Creates ap controllers from a json config. 60 61 Creates an ap controller from either a list, or a single 62 element. The element can either be just the hostname or a dictionary 63 containing the hostname and username of the ap to connect to over ssh. 64 65 Args: 66 The json configs that represent this controller. 67 68 Returns: 69 A new AccessPoint. 70 """ 71 return [AccessPoint(c) for c in configs] 72 73 74def destroy(aps): 75 """Destroys a list of access points. 76 77 Args: 78 aps: The list of access points to destroy. 79 """ 80 for ap in aps: 81 ap.close() 82 83 84def get_info(aps): 85 """Get information on a list of access points. 86 87 Args: 88 aps: A list of AccessPoints. 89 90 Returns: 91 A list of all aps hostname. 92 """ 93 return [ap.ssh_settings.hostname for ap in aps] 94 95 96def setup_ap( 97 access_point, 98 profile_name, 99 channel, 100 ssid, 101 mode=None, 102 preamble=None, 103 beacon_interval=None, 104 dtim_period=None, 105 frag_threshold=None, 106 rts_threshold=None, 107 force_wmm=None, 108 hidden=False, 109 security=None, 110 pmf_support=None, 111 additional_ap_parameters=None, 112 password=None, 113 n_capabilities=None, 114 ac_capabilities=None, 115 vht_bandwidth=None, 116 wnm_features: FrozenSet[hostapd_constants.WnmFeature] = frozenset(), 117 setup_bridge=False, 118 is_ipv6_enabled=False, 119 is_nat_enabled=True): 120 """Creates a hostapd profile and runs it on an ap. This is a convenience 121 function that allows us to start an ap with a single function, without first 122 creating a hostapd config. 123 124 Args: 125 access_point: An ACTS access_point controller 126 profile_name: The profile name of one of the hostapd ap presets. 127 channel: What channel to set the AP to. 128 preamble: Whether to set short or long preamble (True or False) 129 beacon_interval: The beacon interval (int) 130 dtim_period: Length of dtim period (int) 131 frag_threshold: Fragmentation threshold (int) 132 rts_threshold: RTS threshold (int) 133 force_wmm: Enable WMM or not (True or False) 134 hidden: Advertise the SSID or not (True or False) 135 security: What security to enable. 136 pmf_support: int, whether pmf is not disabled, enabled, or required 137 additional_ap_parameters: Additional parameters to send the AP. 138 password: Password to connect to WLAN if necessary. 139 check_connectivity: Whether to check for internet connectivity. 140 wnm_features: WNM features to enable on the AP. 141 setup_bridge: Whether to bridge the LAN interface WLAN interface. 142 Only one WLAN interface can be bridged with the LAN interface 143 and none of the guest networks can be bridged. 144 is_ipv6_enabled: If True, start a IPv6 router advertisement daemon 145 is_nat_enabled: If True, start NAT on the AP to allow the DUT to be able 146 to access the internet if the WAN port is connected to the internet. 147 148 Returns: 149 An identifier for each ssid being started. These identifiers can be 150 used later by this controller to control the ap. 151 152 Raises: 153 Error: When the ap can't be brought up. 154 """ 155 ap = hostapd_ap_preset.create_ap_preset(profile_name=profile_name, 156 iface_wlan_2g=access_point.wlan_2g, 157 iface_wlan_5g=access_point.wlan_5g, 158 channel=channel, 159 ssid=ssid, 160 mode=mode, 161 short_preamble=preamble, 162 beacon_interval=beacon_interval, 163 dtim_period=dtim_period, 164 frag_threshold=frag_threshold, 165 rts_threshold=rts_threshold, 166 force_wmm=force_wmm, 167 hidden=hidden, 168 bss_settings=[], 169 security=security, 170 pmf_support=pmf_support, 171 n_capabilities=n_capabilities, 172 ac_capabilities=ac_capabilities, 173 vht_bandwidth=vht_bandwidth, 174 wnm_features=wnm_features) 175 return access_point.start_ap( 176 hostapd_config=ap, 177 radvd_config=radvd_config.RadvdConfig() if is_ipv6_enabled else None, 178 setup_bridge=setup_bridge, 179 is_nat_enabled=is_nat_enabled, 180 additional_parameters=additional_ap_parameters) 181 182 183class Error(Exception): 184 """Error raised when there is a problem with the access point.""" 185 186 187_ApInstance = collections.namedtuple('_ApInstance', ['hostapd', 'subnet']) 188 189# These ranges were split this way since each physical radio can have up 190# to 8 SSIDs so for the 2GHz radio the DHCP range will be 191# 192.168.1 - 8 and the 5Ghz radio will be 192.168.9 - 16 192_AP_2GHZ_SUBNET_STR_DEFAULT = '192.168.1.0/24' 193_AP_5GHZ_SUBNET_STR_DEFAULT = '192.168.9.0/24' 194 195# The last digit of the ip for the bridge interface 196BRIDGE_IP_LAST = '100' 197 198 199class AccessPoint(object): 200 """An access point controller. 201 202 Attributes: 203 ssh: The ssh connection to this ap. 204 ssh_settings: The ssh settings being used by the ssh connection. 205 dhcp_settings: The dhcp server settings being used. 206 """ 207 208 def __init__(self, configs): 209 """ 210 Args: 211 configs: configs for the access point from config file. 212 """ 213 self.ssh_settings = settings.from_config(configs['ssh_config']) 214 self.log = logger.create_logger( 215 lambda msg: f'[Access Point|{self.ssh_settings.hostname}] {msg}') 216 self.device_pdu_config = configs.get('PduDevice', None) 217 self.identifier = self.ssh_settings.hostname 218 219 if 'ap_subnet' in configs: 220 self._AP_2G_SUBNET_STR = configs['ap_subnet']['2g'] 221 self._AP_5G_SUBNET_STR = configs['ap_subnet']['5g'] 222 else: 223 self._AP_2G_SUBNET_STR = _AP_2GHZ_SUBNET_STR_DEFAULT 224 self._AP_5G_SUBNET_STR = _AP_5GHZ_SUBNET_STR_DEFAULT 225 226 self._AP_2G_SUBNET = dhcp_config.Subnet( 227 ipaddress.ip_network(self._AP_2G_SUBNET_STR)) 228 self._AP_5G_SUBNET = dhcp_config.Subnet( 229 ipaddress.ip_network(self._AP_5G_SUBNET_STR)) 230 231 self.ssh = connection.SshConnection(self.ssh_settings) 232 233 # Singleton utilities for running various commands. 234 self._ip_cmd = ip.LinuxIpCommand(self.ssh) 235 self._route_cmd = route.LinuxRouteCommand(self.ssh) 236 237 # A map from network interface name to _ApInstance objects representing 238 # the hostapd instance running against the interface. 239 self._aps = dict() 240 self._dhcp = None 241 self._dhcp_bss = dict() 242 self._radvd = None 243 self.bridge = bridge_interface.BridgeInterface(self) 244 self.iwconfig = ap_iwconfig.ApIwconfig(self) 245 246 # Check to see if wan_interface is specified in acts_config for tests 247 # isolated from the internet and set this override. 248 self.interfaces = ap_get_interface.ApInterfaces( 249 self, configs.get('wan_interface')) 250 251 # Get needed interface names and initialize the unnecessary ones. 252 self.wan = self.interfaces.get_wan_interface() 253 self.wlan = self.interfaces.get_wlan_interface() 254 self.wlan_2g = self.wlan[0] 255 self.wlan_5g = self.wlan[1] 256 self.lan = self.interfaces.get_lan_interface() 257 self._initial_ap() 258 self.scapy_install_path = None 259 self.setup_bridge = False 260 261 def _initial_ap(self): 262 """Initial AP interfaces. 263 264 Bring down hostapd if instance is running, bring down all bridge 265 interfaces. 266 """ 267 # This is necessary for Gale/Whirlwind flashed with dev channel image 268 # Unused interfaces such as existing hostapd daemon, guest, mesh 269 # interfaces need to be brought down as part of the AP initialization 270 # process, otherwise test would fail. 271 try: 272 self.ssh.run('stop wpasupplicant') 273 except job.Error: 274 self.log.info('No wpasupplicant running') 275 try: 276 self.ssh.run('stop hostapd') 277 except job.Error: 278 self.log.info('No hostapd running') 279 # Bring down all wireless interfaces 280 for iface in self.wlan: 281 WLAN_DOWN = f'ip link set {iface} down' 282 self.ssh.run(WLAN_DOWN) 283 # Bring down all bridge interfaces 284 bridge_interfaces = self.interfaces.get_bridge_interface() 285 if bridge_interfaces: 286 for iface in bridge_interfaces: 287 BRIDGE_DOWN = f'ip link set {iface} down' 288 BRIDGE_DEL = f'brctl delbr {iface}' 289 self.ssh.run(BRIDGE_DOWN) 290 self.ssh.run(BRIDGE_DEL) 291 292 def start_ap(self, 293 hostapd_config, 294 radvd_config=None, 295 setup_bridge=False, 296 is_nat_enabled=True, 297 additional_parameters=None): 298 """Starts as an ap using a set of configurations. 299 300 This will start an ap on this host. To start an ap the controller 301 selects a network interface to use based on the configs given. It then 302 will start up hostapd on that interface. Next a subnet is created for 303 the network interface and dhcp server is refreshed to give out ips 304 for that subnet for any device that connects through that interface. 305 306 Args: 307 hostapd_config: hostapd_config.HostapdConfig, The configurations 308 to use when starting up the ap. 309 radvd_config: radvd_config.RadvdConfig, The IPv6 configuration 310 to use when starting up the ap. 311 setup_bridge: Whether to bridge the LAN interface WLAN interface. 312 Only one WLAN interface can be bridged with the LAN interface 313 and none of the guest networks can be bridged. 314 is_nat_enabled: If True, start NAT on the AP to allow the DUT to be 315 able to access the internet if the WAN port is connected to the 316 internet. 317 additional_parameters: A dictionary of parameters that can sent 318 directly into the hostapd config file. This can be used for 319 debugging and or adding one off parameters into the config. 320 321 Returns: 322 An identifier for each ssid being started. These identifiers can be 323 used later by this controller to control the ap. 324 325 Raises: 326 Error: When the ap can't be brought up. 327 """ 328 if hostapd_config.frequency < 5000: 329 interface = self.wlan_2g 330 subnet = self._AP_2G_SUBNET 331 else: 332 interface = self.wlan_5g 333 subnet = self._AP_5G_SUBNET 334 335 # In order to handle dhcp servers on any interface, the initiation of 336 # the dhcp server must be done after the wlan interfaces are figured 337 # out as opposed to being in __init__ 338 self._dhcp = dhcp_server.DhcpServer(self.ssh, interface=interface) 339 340 # For multi bssid configurations the mac address 341 # of the wireless interface needs to have enough space to mask out 342 # up to 8 different mac addresses. So in for one interface the range is 343 # hex 0-7 and for the other the range is hex 8-f. 344 interface_mac_orig = None 345 cmd = f"ip link show {interface}|grep ether|awk -F' ' '{{print $2}}'" 346 interface_mac_orig = self.ssh.run(cmd) 347 if interface == self.wlan_5g: 348 hostapd_config.bssid = interface_mac_orig.stdout[:-1] + '0' 349 last_octet = 1 350 if interface == self.wlan_2g: 351 hostapd_config.bssid = interface_mac_orig.stdout[:-1] + '8' 352 last_octet = 9 353 if interface in self._aps: 354 raise ValueError('No WiFi interface available for AP on ' 355 f'channel {hostapd_config.channel}') 356 357 apd = hostapd.Hostapd(self.ssh, interface) 358 new_instance = _ApInstance(hostapd=apd, subnet=subnet) 359 self._aps[interface] = new_instance 360 361 # Turn off the DHCP server, we're going to change its settings. 362 self.stop_dhcp() 363 # Clear all routes to prevent old routes from interfering. 364 self._route_cmd.clear_routes(net_interface=interface) 365 366 self._dhcp_bss = dict() 367 if hostapd_config.bss_lookup: 368 # The self._dhcp_bss dictionary is created to hold the key/value 369 # pair of the interface name and the ip scope that will be 370 # used for the particular interface. The a, b, c, d 371 # variables below are the octets for the ip address. The 372 # third octet is then incremented for each interface that 373 # is requested. This part is designed to bring up the 374 # hostapd interfaces and not the DHCP servers for each 375 # interface. 376 counter = 1 377 for bss in hostapd_config.bss_lookup: 378 if interface_mac_orig: 379 hostapd_config.bss_lookup[bss].bssid = ( 380 interface_mac_orig.stdout[:-1] + hex(last_octet)[-1:]) 381 self._route_cmd.clear_routes(net_interface=str(bss)) 382 if interface is self.wlan_2g: 383 starting_ip_range = self._AP_2G_SUBNET_STR 384 else: 385 starting_ip_range = self._AP_5G_SUBNET_STR 386 a, b, c, d = starting_ip_range.split('.') 387 self._dhcp_bss[bss] = dhcp_config.Subnet( 388 ipaddress.ip_network(f'{a}.{b}.{int(c) + counter}.{d}')) 389 counter = counter + 1 390 last_octet = last_octet + 1 391 392 apd.start(hostapd_config, additional_parameters=additional_parameters) 393 394 # The DHCP serer requires interfaces to have ips and routes before 395 # the server will come up. 396 interface_ip = ipaddress.ip_interface( 397 f'{subnet.router}/{subnet.network.netmask}') 398 if setup_bridge is True: 399 bridge_interface_name = 'eth_test' 400 self.create_bridge(bridge_interface_name, [interface, self.lan]) 401 self._ip_cmd.set_ipv4_address(bridge_interface_name, interface_ip) 402 else: 403 self._ip_cmd.set_ipv4_address(interface, interface_ip) 404 if hostapd_config.bss_lookup: 405 # This loop goes through each interface that was setup for 406 # hostapd and assigns the DHCP scopes that were defined but 407 # not used during the hostapd loop above. The k and v 408 # variables represent the interface name, k, and dhcp info, v. 409 for k, v in self._dhcp_bss.items(): 410 bss_interface_ip = ipaddress.ip_interface( 411 f'{self._dhcp_bss[k].router}/{self._dhcp_bss[k].network.netmask}' 412 ) 413 self._ip_cmd.set_ipv4_address(str(k), bss_interface_ip) 414 415 # Restart the DHCP server with our updated list of subnets. 416 configured_subnets = self.get_configured_subnets() 417 dhcp_conf = dhcp_config.DhcpConfig(subnets=configured_subnets) 418 self.start_dhcp(dhcp_conf=dhcp_conf) 419 if is_nat_enabled: 420 self.start_nat() 421 self.enable_forwarding() 422 else: 423 self.stop_nat() 424 self.enable_forwarding() 425 if radvd_config: 426 radvd_interface = bridge_interface_name if setup_bridge else interface 427 self._radvd = radvd.Radvd(self.ssh, radvd_interface) 428 self._radvd.start(radvd_config) 429 430 bss_interfaces = [bss for bss in hostapd_config.bss_lookup] 431 bss_interfaces.append(interface) 432 433 return bss_interfaces 434 435 def get_configured_subnets(self): 436 """Get the list of configured subnets on the access point. 437 438 This allows consumers of the access point objects create custom DHCP 439 configs with the correct subnets. 440 441 Returns: a list of dhcp_config.Subnet objects 442 """ 443 configured_subnets = [x.subnet for x in self._aps.values()] 444 for k, v in self._dhcp_bss.items(): 445 configured_subnets.append(v) 446 return configured_subnets 447 448 def start_dhcp(self, dhcp_conf): 449 """Start a DHCP server for the specified subnets. 450 451 This allows consumers of the access point objects to control DHCP. 452 453 Args: 454 dhcp_conf: A dhcp_config.DhcpConfig object. 455 456 Raises: 457 Error: Raised when a dhcp server error is found. 458 """ 459 self._dhcp.start(config=dhcp_conf) 460 461 def stop_dhcp(self): 462 """Stop DHCP for this AP object. 463 464 This allows consumers of the access point objects to control DHCP. 465 """ 466 self._dhcp.stop() 467 468 def get_dhcp_logs(self): 469 """Get DHCP logs for this AP object. 470 471 This allows consumers of the access point objects to validate DHCP 472 behavior. 473 474 Returns: 475 A string of the dhcp server logs, or None is a DHCP server has not 476 been started. 477 """ 478 if self._dhcp: 479 return self._dhcp.get_logs() 480 return None 481 482 def get_hostapd_logs(self): 483 """Get hostapd logs for all interfaces on AP object. 484 485 This allows consumers of the access point objects to validate hostapd 486 behavior. 487 488 Returns: A dict with {interface: log} from hostapd instances. 489 """ 490 hostapd_logs = dict() 491 for identifier in self._aps: 492 hostapd_logs[identifier] = self._aps.get( 493 identifier).hostapd.pull_logs() 494 return hostapd_logs 495 496 def enable_forwarding(self): 497 """Enable IPv4 and IPv6 forwarding on the AP. 498 499 When forwarding is enabled, the access point is able to route IP packets 500 between devices in the same subnet. 501 """ 502 self.ssh.run('echo 1 > /proc/sys/net/ipv4/ip_forward') 503 self.ssh.run('echo 1 > /proc/sys/net/ipv6/conf/all/forwarding') 504 505 def start_nat(self): 506 """Start NAT on the AP. 507 508 This allows consumers of the access point objects to enable NAT 509 on the AP. 510 511 Note that this is currently a global setting, since we don't 512 have per-interface masquerade rules. 513 """ 514 # The following three commands are needed to enable NAT between 515 # the WAN and LAN/WLAN ports. This means anyone connecting to the 516 # WLAN/LAN ports will be able to access the internet if the WAN port 517 # is connected to the internet. 518 self.ssh.run('iptables -t nat -F') 519 self.ssh.run( 520 f'iptables -t nat -A POSTROUTING -o {self.wan} -j MASQUERADE') 521 522 def stop_nat(self): 523 """Stop NAT on the AP. 524 525 This allows consumers of the access point objects to disable NAT on the 526 AP. 527 528 Note that this is currently a global setting, since we don't have 529 per-interface masquerade rules. 530 """ 531 self.ssh.run('iptables -t nat -F') 532 533 def create_bridge(self, bridge_name, interfaces): 534 """Create the specified bridge and bridge the specified interfaces. 535 536 Args: 537 bridge_name: The name of the bridge to create. 538 interfaces: A list of interfaces to add to the bridge. 539 """ 540 541 # Create the bridge interface 542 self.ssh.run(f'brctl addbr {bridge_name}') 543 544 for interface in interfaces: 545 self.ssh.run(f'brctl addif {bridge_name} {interface}') 546 547 self.ssh.run(f'ip link set {bridge_name} up') 548 549 def remove_bridge(self, bridge_name): 550 """Removes the specified bridge 551 552 Args: 553 bridge_name: The name of the bridge to remove. 554 """ 555 # Check if the bridge exists. 556 # 557 # Cases where it may not are if we failed to initialize properly 558 # 559 # Or if we're doing 2.4Ghz and 5Ghz SSIDs and we've already torn 560 # down the bridge once, but we got called for each band. 561 result = self.ssh.run(f'brctl show {bridge_name}', ignore_status=True) 562 563 # If the bridge exists, we'll get an exit_status of 0, indicating 564 # success, so we can continue and remove the bridge. 565 if result.exit_status == 0: 566 self.ssh.run(f'ip link set {bridge_name} down') 567 self.ssh.run(f'brctl delbr {bridge_name}') 568 569 def get_bssid_from_ssid(self, ssid, band): 570 """Gets the BSSID from a provided SSID 571 572 Args: 573 ssid: An SSID string. 574 band: 2G or 5G Wifi band. 575 Returns: The BSSID if on the AP or None if SSID could not be found. 576 """ 577 if band == hostapd_constants.BAND_2G: 578 interfaces = [self.wlan_2g, ssid] 579 else: 580 interfaces = [self.wlan_5g, ssid] 581 582 # Get the interface name associated with the given ssid. 583 for interface in interfaces: 584 iw_output = self.ssh.run( 585 f"iw dev {interface} info|grep ssid|awk -F' ' '{{print $2}}'") 586 if 'command failed: No such device' in iw_output.stderr: 587 continue 588 else: 589 # If the configured ssid is equal to the given ssid, we found 590 # the right interface. 591 if iw_output.stdout == ssid: 592 iw_output = self.ssh.run( 593 f"iw dev {interface} info|grep addr|awk -F' ' '{{print $2}}'" 594 ) 595 return iw_output.stdout 596 return None 597 598 def stop_ap(self, identifier): 599 """Stops a running ap on this controller. 600 601 Args: 602 identifier: The identify of the ap that should be taken down. 603 """ 604 605 if identifier not in list(self._aps.keys()): 606 raise ValueError(f'Invalid identifier {identifier} given') 607 608 instance = self._aps.get(identifier) 609 610 if self._radvd: 611 self._radvd.stop() 612 try: 613 self.stop_dhcp() 614 except dhcp_server.NoInterfaceError: 615 pass 616 self.stop_nat() 617 instance.hostapd.stop() 618 self._ip_cmd.clear_ipv4_addresses(identifier) 619 620 del self._aps[identifier] 621 bridge_interfaces = self.interfaces.get_bridge_interface() 622 if bridge_interfaces: 623 for iface in bridge_interfaces: 624 BRIDGE_DOWN = f'ip link set {iface} down' 625 BRIDGE_DEL = f'brctl delbr {iface}' 626 self.ssh.run(BRIDGE_DOWN) 627 self.ssh.run(BRIDGE_DEL) 628 629 def stop_all_aps(self): 630 """Stops all running aps on this device.""" 631 632 for ap in list(self._aps.keys()): 633 self.stop_ap(ap) 634 635 def close(self): 636 """Called to take down the entire access point. 637 638 When called will stop all aps running on this host, shutdown the dhcp 639 server, and stop the ssh connection. 640 """ 641 642 if self._aps: 643 self.stop_all_aps() 644 self.ssh.close() 645 646 def generate_bridge_configs(self, channel): 647 """Generate a list of configs for a bridge between LAN and WLAN. 648 649 Args: 650 channel: the channel WLAN interface is brought up on 651 iface_lan: the LAN interface to bridge 652 Returns: 653 configs: tuple containing iface_wlan, iface_lan and bridge_ip 654 """ 655 656 if channel < 15: 657 iface_wlan = self.wlan_2g 658 subnet_str = self._AP_2G_SUBNET_STR 659 else: 660 iface_wlan = self.wlan_5g 661 subnet_str = self._AP_5G_SUBNET_STR 662 663 iface_lan = self.lan 664 665 a, b, c, _ = subnet_str.strip('/24').split('.') 666 bridge_ip = f'{a}.{b}.{c}.{BRIDGE_IP_LAST}' 667 668 configs = (iface_wlan, iface_lan, bridge_ip) 669 670 return configs 671 672 def install_scapy(self, scapy_path, send_ra_path): 673 """Install scapy 674 675 Args: 676 scapy_path: path where scapy tar file is located on server 677 send_ra_path: path where sendra path is located on server 678 """ 679 self.scapy_install_path = self.ssh.run('mktemp -d').stdout.rstrip() 680 self.log.info(f'Scapy install path: {self.scapy_install_path}') 681 self.ssh.send_file(scapy_path, self.scapy_install_path) 682 self.ssh.send_file(send_ra_path, self.scapy_install_path) 683 684 scapy = os.path.join(self.scapy_install_path, 685 scapy_path.split('/')[-1]) 686 687 self.ssh.run(f'tar -xvf {scapy} -C {self.scapy_install_path}') 688 self.ssh.run(f'cd {self.scapy_install_path}; {SCAPY_INSTALL_COMMAND}') 689 690 def cleanup_scapy(self): 691 """ Cleanup scapy """ 692 if self.scapy_install_path: 693 cmd = f'rm -rf {self.scapy_install_path}' 694 self.log.info(f'Cleaning up scapy {cmd}') 695 output = self.ssh.run(cmd) 696 self.scapy_install_path = None 697 698 def send_ra(self, 699 iface, 700 mac=RA_MULTICAST_ADDR, 701 interval=1, 702 count=None, 703 lifetime=LIFETIME, 704 rtt=0): 705 """Invoke scapy and send RA to the device. 706 707 Args: 708 iface: string of the WiFi interface to use for sending packets. 709 mac: string HWAddr/MAC address to send the packets to. 710 interval: int Time to sleep between consecutive packets. 711 count: int Number of packets to be sent. 712 lifetime: int original RA's router lifetime in seconds. 713 rtt: retrans timer of the RA packet 714 """ 715 scapy_command = os.path.join(self.scapy_install_path, RA_SCRIPT) 716 options = f' -m {mac} -i {interval} -c {count} -l {lifetime} -in {iface} -rtt {rtt}' 717 cmd = scapy_command + options 718 self.log.info(f'Scapy cmd: {cmd}') 719 self.ssh.run(cmd) 720 721 def get_icmp6intype134(self): 722 """Read the value of Icmp6InType134 and return integer. 723 724 Returns: 725 Integer value >0 if grep is successful; 0 otherwise. 726 """ 727 ra_count_str = self.ssh.run( 728 f'grep Icmp6InType134 {PROC_NET_SNMP6} || true').stdout 729 if ra_count_str: 730 return int(ra_count_str.split()[1]) 731 732 def ping(self, 733 dest_ip, 734 count=3, 735 interval=1000, 736 timeout=1000, 737 size=56, 738 additional_ping_params=None): 739 """Pings from AP to dest_ip, returns dict of ping stats (see utils.ping) 740 """ 741 return utils.ping(self.ssh, 742 dest_ip, 743 count=count, 744 interval=interval, 745 timeout=timeout, 746 size=size, 747 additional_ping_params=additional_ping_params) 748 749 def can_ping(self, 750 dest_ip, 751 count=1, 752 interval=1000, 753 timeout=1000, 754 size=56, 755 additional_ping_params=None): 756 """Returns whether ap can ping dest_ip (see utils.can_ping)""" 757 return utils.can_ping(self.ssh, 758 dest_ip, 759 count=count, 760 interval=interval, 761 timeout=timeout, 762 size=size, 763 additional_ping_params=additional_ping_params) 764 765 def hard_power_cycle(self, 766 pdus, 767 unreachable_timeout=30, 768 ping_timeout=60, 769 ssh_timeout=30, 770 hostapd_configs=None): 771 """Kills, then restores power to AccessPoint, verifying it goes down and 772 comes back online cleanly. 773 774 Args: 775 pdus: list, PduDevices in the testbed 776 unreachable_timeout: int, time to wait for AccessPoint to become 777 unreachable 778 ping_timeout: int, time to wait for AccessPoint to responsd to pings 779 ssh_timeout: int, time to wait for AccessPoint to allow SSH 780 hostapd_configs (optional): list, containing hostapd settings. If 781 present, these networks will be spun up after the AP has 782 rebooted. This list can either contain HostapdConfig objects, or 783 dictionaries with the start_ap params 784 (i.e { 'hostapd_config': <HostapdConfig>, 785 'setup_bridge': <bool>, 786 'additional_parameters': <dict> } ). 787 Raise: 788 Error, if no PduDevice is provided in AccessPoint config. 789 ConnectionError, if AccessPoint fails to go offline or come back. 790 """ 791 if not self.device_pdu_config: 792 raise Error('No PduDevice provided in AccessPoint config.') 793 794 if hostapd_configs is None: 795 hostapd_configs = [] 796 797 self.log.info(f'Power cycling') 798 ap_pdu, ap_pdu_port = pdu.get_pdu_port_for_device( 799 self.device_pdu_config, pdus) 800 801 self.log.info(f'Killing power') 802 ap_pdu.off(str(ap_pdu_port)) 803 804 self.log.info('Verifying AccessPoint is unreachable.') 805 timeout = time.time() + unreachable_timeout 806 while time.time() < timeout: 807 if not utils.can_ping(job, self.ssh_settings.hostname): 808 self.log.info('AccessPoint is unreachable as expected.') 809 break 810 else: 811 self.log.debug( 812 'AccessPoint is still responding to pings. Retrying in 1 ' 813 'second.') 814 time.sleep(1) 815 else: 816 raise ConnectionError( 817 f'Failed to bring down AccessPoint ({self.ssh_settings.hostname})' 818 ) 819 self._aps.clear() 820 821 self.log.info(f'Restoring power') 822 ap_pdu.on(str(ap_pdu_port)) 823 824 self.log.info('Waiting for AccessPoint to respond to pings.') 825 timeout = time.time() + ping_timeout 826 while time.time() < timeout: 827 if utils.can_ping(job, self.ssh_settings.hostname): 828 self.log.info('AccessPoint responded to pings.') 829 break 830 else: 831 self.log.debug('AccessPoint is not responding to pings. ' 832 'Retrying in 1 second.') 833 time.sleep(1) 834 else: 835 raise ConnectionError( 836 f'Timed out waiting for AccessPoint ({self.ssh_settings.hostname}) ' 837 'to respond to pings.') 838 839 self.log.info('Waiting for AccessPoint to allow ssh connection.') 840 timeout = time.time() + ssh_timeout 841 while time.time() < timeout: 842 try: 843 self.ssh.run('echo') 844 except connection.Error: 845 self.log.debug('AccessPoint is not allowing ssh connection. ' 846 'Retrying in 1 second.') 847 time.sleep(1) 848 else: 849 self.log.info('AccessPoint available via ssh.') 850 break 851 else: 852 raise ConnectionError( 853 f'Timed out waiting for AccessPoint ({self.ssh_settings.hostname}) ' 854 'to allow ssh connection.') 855 856 # Allow 5 seconds for OS to finish getting set up 857 time.sleep(5) 858 self._initial_ap() 859 self.log.info('Power cycled successfully') 860 861 for settings in hostapd_configs: 862 if type(settings) == hostapd_config.HostapdConfig: 863 config = settings 864 setup_bridge = False 865 additional_parameters = None 866 867 elif type(settings) == dict: 868 config = settings['hostapd_config'] 869 setup_bridge = settings.get('setup_bridge', False) 870 additional_parameters = settings.get('additional_parameters', 871 None) 872 else: 873 raise TypeError( 874 'Items in hostapd_configs list must either be ' 875 'hostapd.HostapdConfig objects or dictionaries.') 876 877 self.log.info(f'Restarting network {config.ssid}') 878 self.start_ap(config, 879 setup_bridge=setup_bridge, 880 additional_parameters=additional_parameters) 881 882 def channel_switch(self, identifier, channel_num): 883 """Switch to a different channel on the given AP.""" 884 if identifier not in list(self._aps.keys()): 885 raise ValueError(f'Invalid identifier {identifier} given') 886 instance = self._aps.get(identifier) 887 self.log.info(f'channel switch to channel {channel_num}') 888 instance.hostapd.channel_switch(channel_num) 889 890 def get_current_channel(self, identifier): 891 """Find the current channel on the given AP.""" 892 if identifier not in list(self._aps.keys()): 893 raise ValueError(f'Invalid identifier {identifier} given') 894 instance = self._aps.get(identifier) 895 return instance.hostapd.get_current_channel() 896 897 def get_stas(self, identifier) -> Set[str]: 898 """Return MAC addresses of all associated STAs on the given AP.""" 899 if identifier not in list(self._aps.keys()): 900 raise ValueError(f'Invalid identifier {identifier} given') 901 instance = self._aps.get(identifier) 902 return instance.hostapd.get_stas() 903 904 def get_sta_extended_capabilities(self, identifier, 905 sta_mac: str) -> ExtendedCapabilities: 906 """Get extended capabilities for the given STA, as seen by the AP.""" 907 if identifier not in list(self._aps.keys()): 908 raise ValueError(f'Invalid identifier {identifier} given') 909 instance = self._aps.get(identifier) 910 return instance.hostapd.get_sta_extended_capabilities(sta_mac) 911 912 def send_bss_transition_management_req( 913 self, identifier, sta_mac: str, 914 request: BssTransitionManagementRequest): 915 """Send a BSS Transition Management request to an associated STA.""" 916 if identifier not in list(self._aps.keys()): 917 raise ValueError('Invalid identifier {identifier} given') 918 instance = self._aps.get(identifier) 919 return instance.hostapd.send_bss_transition_management_req( 920 sta_mac, request) 921