1# Copyright 2016 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import atexit 6import logging 7import os 8import re 9import socket 10import struct 11import subprocess 12import sys 13 14# pylint: disable=wrong-import-position 15REPOSITORY_ROOT = os.path.abspath(os.path.join( 16 os.path.dirname(__file__), '..', '..', '..')) 17sys.path.append(os.path.join(REPOSITORY_ROOT, 'tools', 'perf')) 18from core import path_util 19sys.path.append(path_util.GetTelemetryDir()) 20 21from telemetry.core import platform 22from telemetry.internal.platform import android_device 23from telemetry.internal.util import binary_manager 24 25from devil.android import device_errors 26from devil.android import device_utils 27 28import py_utils 29# pylint: enable=wrong-import-position 30 31# pylint: disable=useless-object-inheritance 32 33 34class AndroidRndisForwarder(object): 35 """Forwards traffic using RNDIS. Assumes the device has root access.""" 36 37 def __init__(self, device, rndis_configurator): 38 self._device = device 39 self._rndis_configurator = rndis_configurator 40 self._device_iface = rndis_configurator.device_iface 41 self._host_ip = rndis_configurator.host_ip 42 self._original_dns = None, None, None 43 self._RedirectPorts() 44 # The netd commands fail on Lollipop and newer releases, but aren't 45 # necessary as DNS isn't used. 46 # self._OverrideDns() 47 self._OverrideDefaultGateway() 48 # Need to override routing policy again since call to setifdns 49 # sometimes resets policy table 50 self._rndis_configurator.OverrideRoutingPolicy() 51 atexit.register(self.Close) 52 # TODO(tonyg): Verify that each port can connect to host. 53 54 @property 55 def host_ip(self): 56 return self._host_ip 57 58 def Close(self): 59 #if self._forwarding: 60 # self._rndis_configurator.RestoreRoutingPolicy() 61 # self._SetDns(*self._original_dns) 62 # self._RestoreDefaultGateway() 63 #super(AndroidRndisForwarder, self).Close() 64 pass 65 66 def _RedirectPorts(self): 67 """Sets the local to remote pair mappings to use for RNDIS.""" 68 # Flush any old nat rules. 69 self._device.RunShellCommand( 70 ['iptables', '-F', '-t', 'nat'], check_return=True) 71 72 def _OverrideDns(self): 73 """Overrides DNS on device to point at the host.""" 74 self._original_dns = self._GetCurrentDns() 75 self._SetDns(self._device_iface, self.host_ip, self.host_ip) 76 77 def _SetDns(self, iface, dns1, dns2): 78 """Overrides device's DNS configuration. 79 80 Args: 81 iface: name of the network interface to make default 82 dns1, dns2: nameserver IP addresses 83 """ 84 if not iface: 85 return # If there is no route, then nobody cares about DNS. 86 # DNS proxy in older versions of Android is configured via properties. 87 # TODO(szym): run via su -c if necessary. 88 self._device.SetProp('net.dns1', dns1) 89 self._device.SetProp('net.dns2', dns2) 90 dnschange = self._device.GetProp('net.dnschange') 91 if dnschange: 92 self._device.SetProp('net.dnschange', str(int(dnschange) + 1)) 93 # Since commit 8b47b3601f82f299bb8c135af0639b72b67230e6 to frameworks/base 94 # the net.dns1 properties have been replaced with explicit commands for netd 95 self._device.RunShellCommand( 96 ['netd', 'resolver', 'setifdns', iface, dns1, dns2], check_return=True) 97 # TODO(szym): if we know the package UID, we could setifaceforuidrange 98 self._device.RunShellCommand( 99 ['netd', 'resolver', 'setdefaultif', iface], check_return=True) 100 101 def _GetCurrentDns(self): 102 """Returns current gateway, dns1, and dns2.""" 103 routes = self._device.RunShellCommand( 104 ['cat', '/proc/net/route'], check_return=True)[1:] 105 routes = [route.split() for route in routes] 106 default_routes = [route[0] for route in routes if route[1] == '00000000'] 107 return ( 108 default_routes[0] if default_routes else None, 109 self._device.GetProp('net.dns1'), 110 self._device.GetProp('net.dns2'), 111 ) 112 113 def _OverrideDefaultGateway(self): 114 """Force traffic to go through RNDIS interface. 115 116 Override any default gateway route. Without this traffic may go through 117 the wrong interface. 118 119 This introduces the risk that _RestoreDefaultGateway() is not called 120 (e.g. Telemetry crashes). A power cycle or "adb reboot" is a simple 121 workaround around in that case. 122 """ 123 # NOTE(pauljensen): On Nougat this can produce a weird message and return 124 # a non-zero value, but routing still seems fine, so don't check_return. 125 self._device.RunShellCommand( 126 ['route', 'add', 'default', 'gw', self.host_ip, 127 'dev', self._device_iface]) 128 129 def _RestoreDefaultGateway(self): 130 self._device.RunShellCommand( 131 ['netcfg', self._device_iface, 'down'], check_return=True) 132 133 134class AndroidRndisConfigurator(object): 135 """Configures a linux host to connect to an android device via RNDIS. 136 137 Note that we intentionally leave RNDIS running on the device. This is 138 because the setup is slow and potentially flaky and leaving it running 139 doesn't seem to interfere with any other developer or bot use-cases. 140 """ 141 142 _RNDIS_DEVICE = '/sys/class/android_usb/android0' 143 _NETWORK_INTERFACES = '/etc/network/interfaces' 144 _INTERFACES_INCLUDE = 'source /etc/network/interfaces.d/*.conf' 145 _TELEMETRY_INTERFACE_FILE = '/etc/network/interfaces.d/telemetry-{}.conf' 146 _DEVICE_IP_ADDRESS = '192.168.123.2' 147 148 def __init__(self, device): 149 self._device = device 150 151 try: 152 self._device.EnableRoot() 153 except device_errors.CommandFailedError: 154 logging.error('RNDIS forwarding requires a rooted device.') 155 raise 156 157 self._device_ip = None 158 self._host_iface = None 159 self._host_ip = None 160 self.device_iface = None 161 162 if platform.GetHostPlatform().GetOSName() == 'mac': 163 self._InstallHorndis(platform.GetHostPlatform().GetArchName()) 164 165 assert self._IsRndisSupported(), 'Device does not support RNDIS.' 166 self._CheckConfigureNetwork() 167 168 @property 169 def host_ip(self): 170 return self._host_ip 171 172 def _IsRndisSupported(self): 173 """Checks that the device has RNDIS support in the kernel.""" 174 return self._device.FileExists('%s/f_rndis/device' % self._RNDIS_DEVICE) 175 176 # pylint: disable=inconsistent-return-statements 177 def _FindDeviceRndisInterface(self): 178 """Returns the name of the RNDIS network interface if present.""" 179 config = self._device.RunShellCommand( 180 ['ip', '-o', 'link', 'show'], check_return=True) 181 interfaces = [line.split(':')[1].strip() for line in config] 182 candidates = [iface for iface in interfaces if re.match('rndis|usb', iface)] 183 if candidates: 184 candidates.sort() 185 if len(candidates) == 2 and candidates[0].startswith('rndis') and \ 186 candidates[1].startswith('usb'): 187 return candidates[0] 188 assert len(candidates) == 1, 'Found more than one rndis device!' 189 return candidates[0] 190 # pylint: enable=inconsistent-return-statements 191 192 def _FindDeviceRndisMacAddress(self, interface): 193 """Returns the MAC address of the RNDIS network interface if present.""" 194 config = self._device.RunShellCommand( 195 ['ip', '-o', 'link', 'show', interface], check_return=True)[0] 196 return config.split('link/ether ')[1][:17] 197 198 def _EnumerateHostInterfaces(self): 199 host_platform = platform.GetHostPlatform().GetOSName() 200 if host_platform == 'linux': 201 return subprocess.check_output(['ip', 'addr'], 202 encoding='utf8').splitlines() 203 if host_platform == 'mac': 204 return subprocess.check_output(['ifconfig'], encoding='utf8').splitlines() 205 raise NotImplementedError('Platform %s not supported!' % host_platform) 206 207 # pylint: disable=inconsistent-return-statements 208 def _FindHostRndisInterface(self, device_mac_address): 209 """Returns the name of the host-side network interface.""" 210 interface_list = self._EnumerateHostInterfaces() 211 ether_address = self._device.ReadFile( 212 '%s/f_rndis/ethaddr' % self._RNDIS_DEVICE, 213 as_root=True, force_pull=True).strip() 214 interface_name = None 215 for line in interface_list: 216 if not line.startswith((' ', '\t')): 217 interface_name = line.split(':')[-2].strip() 218 # Attempt to ping device to trigger ARP for device. 219 with open(os.devnull, 'wb') as devnull: 220 subprocess.call(['ping', '-w1', '-c1', '-I', interface_name, 221 self._DEVICE_IP_ADDRESS], stdout=devnull, stderr=devnull) 222 # Check if ARP cache now has device in it. 223 arp = subprocess.check_output( 224 ['arp', '-i', interface_name, self._DEVICE_IP_ADDRESS], 225 encoding='utf8') 226 if device_mac_address in arp: 227 return interface_name 228 elif ether_address in line: 229 return interface_name 230 # NOTE(pauljensen): |ether_address| seems incorrect on Nougat devices, 231 # but just going by the host interface name seems safe enough. 232 elif interface_name == 'usb0': 233 return interface_name 234 # pylint: enable=inconsistent-return-statements 235 236 def _WriteProtectedFile(self, file_path, contents): 237 subprocess.check_call( 238 ['/usr/bin/sudo', 'bash', '-c', 239 'echo -e "%s" > %s' % (contents, file_path)]) 240 241 def _LoadInstalledHoRNDIS(self): 242 """Attempt to load HoRNDIS if installed. 243 If kext could not be loaded or if HoRNDIS is not installed, return False. 244 """ 245 if not os.path.isdir('/System/Library/Extensions/HoRNDIS.kext'): 246 logging.info('HoRNDIS not present on system.') 247 return False 248 249 def HoRNDISLoaded(): 250 return 'HoRNDIS' in subprocess.check_output(['kextstat'], encoding='utf8') 251 252 if HoRNDISLoaded(): 253 return True 254 255 logging.info('HoRNDIS installed but not running, trying to load manually.') 256 subprocess.check_call( 257 ['/usr/bin/sudo', 'kextload', '-b', 'com.joshuawise.kexts.HoRNDIS']) 258 259 return HoRNDISLoaded() 260 261 def _InstallHorndis(self, arch_name): 262 if self._LoadInstalledHoRNDIS(): 263 logging.info('HoRNDIS kext loaded successfully.') 264 return 265 logging.info('Installing HoRNDIS...') 266 pkg_path = binary_manager.FetchPath('horndis', 'mac', arch_name) 267 subprocess.check_call( 268 ['/usr/bin/sudo', 'installer', '-pkg', pkg_path, '-target', '/']) 269 270 def _DisableRndis(self): 271 # Set expect_status=None as this will temporarily break the adb connection. 272 self._device.adb.Shell('setprop sys.usb.config adb', expect_status=None) 273 self._device.WaitUntilFullyBooted() 274 275 def _EnableRndis(self): 276 """Enables the RNDIS network interface.""" 277 script_prefix = '/data/local/tmp/rndis' 278 # This could be accomplished via "svc usb setFunction rndis" but only on 279 # devices which have the "USB tethering" feature. 280 # Also, on some devices, it's necessary to go through "none" function. 281 script = """ 282trap '' HUP 283trap '' TERM 284trap '' PIPE 285 286function manual_config() { 287 echo %(functions)s > %(dev)s/functions 288 echo 224 > %(dev)s/bDeviceClass 289 echo 1 > %(dev)s/enable 290 start adbd 291 setprop sys.usb.state %(functions)s 292} 293 294# This function kills adb transport, so it has to be run "detached". 295function doit() { 296 setprop sys.usb.config none 297 while [ `getprop sys.usb.state` != "none" ]; do 298 sleep 1 299 done 300 manual_config 301 # For some combinations of devices and host kernels, adb won't work unless the 302 # interface is up, but if we bring it up immediately, it will break adb. 303 #sleep 1 304 if ip link show rndis0 ; then 305 ifconfig rndis0 %(device_ip_address)s netmask 255.255.255.0 up 306 else 307 ifconfig usb0 %(device_ip_address)s netmask 255.255.255.0 up 308 fi 309 echo DONE >> %(prefix)s.log 310} 311 312doit & 313 """ % {'dev': self._RNDIS_DEVICE, 314 'functions': 'rndis,adb', 315 'prefix': script_prefix, 316 'device_ip_address': self._DEVICE_IP_ADDRESS} 317 script_file = '%s.sh' % script_prefix 318 log_file = '%s.log' % script_prefix 319 self._device.WriteFile(script_file, script) 320 # TODO(szym): run via su -c if necessary. 321 self._device.RemovePath(log_file, force=True) 322 self._device.RunShellCommand(['.', script_file], check_return=True) 323 self._device.WaitUntilFullyBooted() 324 result = self._device.ReadFile(log_file).splitlines() 325 assert any('DONE' in line for line in result), 'RNDIS script did not run!' 326 327 def _CheckEnableRndis(self, force): 328 """Enables the RNDIS network interface, retrying if necessary. 329 Args: 330 force: Disable RNDIS first, even if it appears already enabled. 331 Returns: 332 device_iface: RNDIS interface name on the device 333 host_iface: corresponding interface name on the host 334 """ 335 for _ in range(3): 336 if not force: 337 device_iface = self._FindDeviceRndisInterface() 338 if device_iface: 339 device_mac_address = self._FindDeviceRndisMacAddress(device_iface) 340 host_iface = self._FindHostRndisInterface(device_mac_address) 341 if host_iface: 342 return device_iface, host_iface 343 self._DisableRndis() 344 self._EnableRndis() 345 force = False 346 raise Exception('Could not enable RNDIS, giving up.') 347 348 def _Ip2Long(self, addr): 349 return struct.unpack('!L', socket.inet_aton(addr))[0] 350 351 def _IpPrefix2AddressMask(self, addr): 352 def _Length2Mask(length): 353 return 0xFFFFFFFF & ~((1 << (32 - length)) - 1) 354 355 addr, masklen = addr.split('/') 356 return self._Ip2Long(addr), _Length2Mask(int(masklen)) 357 358 def _GetHostAddresses(self, iface): 359 """Returns the IP addresses on host's interfaces, breaking out |iface|.""" 360 interface_list = self._EnumerateHostInterfaces() 361 addresses = [] 362 iface_address = None 363 found_iface = False 364 for line in interface_list: 365 if not line.startswith((' ', '\t')): 366 found_iface = iface in line 367 match = re.search(r'(?<=inet )\S+', line) 368 if match: 369 address = match.group(0) 370 if '/' in address: 371 address = self._IpPrefix2AddressMask(address) 372 else: 373 match = re.search(r'(?<=netmask )\S+', line) 374 address = self._Ip2Long(address), int(match.group(0), 16) 375 if found_iface: 376 assert not iface_address, ( 377 'Found %s twice when parsing host interfaces.' % iface) 378 iface_address = address 379 else: 380 addresses.append(address) 381 return addresses, iface_address 382 383 def _GetDeviceAddresses(self, excluded_iface): 384 """Returns the IP addresses on all connected devices. 385 Excludes interface |excluded_iface| on the selected device. 386 """ 387 my_device = str(self._device) 388 addresses = [] 389 for device_serial in android_device.GetDeviceSerials(None): 390 try: 391 device = device_utils.DeviceUtils(device_serial) 392 if device_serial == my_device: 393 excluded = excluded_iface 394 else: 395 excluded = 'no interfaces excluded on other devices' 396 output = device.RunShellCommand( 397 ['ip', '-o', '-4', 'addr'], check_return=True) 398 addresses += [ 399 line.split()[3] for line in output if excluded not in line] 400 except device_errors.CommandFailedError: 401 logging.warning('Unable to determine IP addresses for %s', 402 device_serial) 403 return addresses 404 405 def _ConfigureNetwork(self, device_iface, host_iface): 406 """Configures the |device_iface| to be on the same network as |host_iface|. 407 """ 408 def _Long2Ip(value): 409 return socket.inet_ntoa(struct.pack('!L', value)) 410 411 def _IsNetworkUnique(network, addresses): 412 return all((addr & mask != network & mask) for addr, mask in addresses) 413 414 # pylint: disable=inconsistent-return-statements 415 def _NextUnusedAddress(network, netmask, used_addresses): 416 # Excludes '0' and broadcast. 417 for suffix in range(1, 0xFFFFFFFF & ~netmask): 418 candidate = network | suffix 419 if candidate not in used_addresses: 420 return candidate 421 # pylint: enable=inconsistent-return-statements 422 423 def HasHostAddress(): 424 _, host_address = self._GetHostAddresses(host_iface) 425 return bool(host_address) 426 427 if not HasHostAddress(): 428 if platform.GetHostPlatform().GetOSName() == 'mac': 429 if 'Telemetry' not in subprocess.check_output( 430 ['networksetup', '-listallnetworkservices'], encoding='utf8'): 431 subprocess.check_call( 432 ['/usr/bin/sudo', 'networksetup', 433 '-createnetworkservice', 'Telemetry', host_iface]) 434 subprocess.check_call( 435 ['/usr/bin/sudo', 'networksetup', 436 '-setmanual', 'Telemetry', '192.168.123.1', '255.255.255.0']) 437 elif platform.GetHostPlatform().GetOSName() == 'linux': 438 with open(self._NETWORK_INTERFACES) as f: 439 orig_interfaces = f.read() 440 if self._INTERFACES_INCLUDE not in orig_interfaces: 441 interfaces = '\n'.join([ 442 orig_interfaces, 443 '', 444 '# Added by Telemetry.', 445 self._INTERFACES_INCLUDE]) 446 self._WriteProtectedFile(self._NETWORK_INTERFACES, interfaces) 447 interface_conf_file = self._TELEMETRY_INTERFACE_FILE.format(host_iface) 448 if not os.path.exists(interface_conf_file): 449 interface_conf_dir = os.path.dirname(interface_conf_file) 450 if not os.path.exists(interface_conf_dir): 451 subprocess.call(['/usr/bin/sudo', '/bin/mkdir', interface_conf_dir]) 452 subprocess.call( 453 ['/usr/bin/sudo', '/bin/chmod', '755', interface_conf_dir]) 454 interface_conf = '\n'.join([ 455 '# Added by Telemetry for RNDIS forwarding.', 456 'allow-hotplug %s' % host_iface, 457 'iface %s inet static' % host_iface, 458 ' address 192.168.123.1', 459 ' netmask 255.255.255.0', 460 ]) 461 self._WriteProtectedFile(interface_conf_file, interface_conf) 462 subprocess.check_call(['/usr/bin/sudo', 'ifup', host_iface]) 463 logging.info('Waiting for RNDIS connectivity...') 464 py_utils.WaitFor(HasHostAddress, 30) 465 466 addresses, host_address = self._GetHostAddresses(host_iface) 467 assert host_address, 'Interface %s could not be configured.' % host_iface 468 469 host_ip, netmask = host_address # pylint: disable=unpacking-non-sequence 470 network = host_ip & netmask 471 472 if not _IsNetworkUnique(network, addresses): 473 logging.warning( 474 'The IP address configuration %s of %s is not unique!\n' 475 'Check your /etc/network/interfaces. If this overlap is intended,\n' 476 'you might need to use: ip rule add from <device_ip> lookup <table>\n' 477 'or add the interface to a bridge in order to route to this network.', 478 host_address, host_iface 479 ) 480 481 # Find unused IP address. 482 used_addresses = [addr for addr, _ in addresses] 483 used_addresses += [self._IpPrefix2AddressMask(addr)[0] 484 for addr in self._GetDeviceAddresses(device_iface)] 485 used_addresses += [host_ip] 486 487 device_ip = _NextUnusedAddress(network, netmask, used_addresses) 488 assert device_ip, ('The network %s on %s is full.' % 489 (host_address, host_iface)) 490 491 host_ip = _Long2Ip(host_ip) 492 device_ip = _Long2Ip(device_ip) 493 netmask = _Long2Ip(netmask) 494 495 # TODO(szym) run via su -c if necessary. 496 self._device.RunShellCommand( 497 ['ifconfig', device_iface, device_ip, 'netmask', netmask, 'up'], 498 check_return=True) 499 # Enabling the interface sometimes breaks adb. 500 self._device.WaitUntilFullyBooted() 501 self._host_iface = host_iface 502 self._host_ip = host_ip 503 self.device_iface = device_iface 504 self._device_ip = device_ip 505 506 def _TestConnectivity(self): 507 with open(os.devnull, 'wb') as devnull: 508 return subprocess.call(['ping', '-q', '-c1', '-W1', self._device_ip], 509 stdout=devnull) == 0 510 511 def OverrideRoutingPolicy(self): 512 """Override any routing policy that could prevent 513 packets from reaching the rndis interface 514 """ 515 policies = self._device.RunShellCommand(['ip', 'rule'], check_return=True) 516 if len(policies) > 1 and not 'lookup main' in policies[1]: 517 self._device.RunShellCommand( 518 ['ip', 'rule', 'add', 'prio', '1', 'from', 'all', 'table', 'main'], 519 check_return=True) 520 self._device.RunShellCommand( 521 ['ip', 'route', 'flush', 'cache'], check_return=True) 522 523 def RestoreRoutingPolicy(self): 524 policies = self._device.RunShellCommand(['ip', 'rule'], check_return=True) 525 if len(policies) > 1 and re.match("^1:.*lookup main", policies[1]): 526 self._device.RunShellCommand( 527 ['ip', 'rule', 'del', 'prio', '1'], check_return=True) 528 self._device.RunShellCommand( 529 ['ip', 'route', 'flush', 'cache'], check_return=True) 530 531 def _CheckConfigureNetwork(self): 532 """Enables RNDIS and configures it, retrying until we have connectivity.""" 533 force = False 534 for _ in range(3): 535 device_iface, host_iface = self._CheckEnableRndis(force) 536 self._ConfigureNetwork(device_iface, host_iface) 537 self.OverrideRoutingPolicy() 538 # Sometimes the first packet will wake up the connection. 539 for _ in range(3): 540 if self._TestConnectivity(): 541 return 542 force = True 543 self.RestoreRoutingPolicy() 544 raise Exception('No connectivity, giving up.') 545