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