Index: tools/telemetry/telemetry/internal/forwarders/android_forwarder.py |
diff --git a/tools/telemetry/telemetry/internal/forwarders/android_forwarder.py b/tools/telemetry/telemetry/internal/forwarders/android_forwarder.py |
deleted file mode 100644 |
index 34839c921ff1265b6a46d3fc8d7f3aa167202812..0000000000000000000000000000000000000000 |
--- a/tools/telemetry/telemetry/internal/forwarders/android_forwarder.py |
+++ /dev/null |
@@ -1,573 +0,0 @@ |
-# Copyright 2014 The Chromium Authors. All rights reserved. |
-# Use of this source code is governed by a BSD-style license that can be |
-# found in the LICENSE file. |
- |
-import atexit |
-import logging |
-import os |
-import re |
-import socket |
-import struct |
-import subprocess |
- |
-from telemetry.internal.util import binary_manager |
-from telemetry.core import platform |
-from telemetry.core import util |
-from telemetry.internal import forwarders |
-from telemetry.internal.platform import android_device |
- |
-from devil.android import device_errors |
-from devil.android import device_utils |
- |
-try: |
- from devil.android import forwarder |
-except ImportError: |
- forwarder = None |
- |
- |
-class AndroidForwarderFactory(forwarders.ForwarderFactory): |
- |
- def __init__(self, device, use_rndis): |
- super(AndroidForwarderFactory, self).__init__() |
- self._device = device |
- self._rndis_configurator = None |
- if use_rndis: |
- self._rndis_configurator = AndroidRndisConfigurator(self._device) |
- |
- def Create(self, port_pairs): |
- try: |
- if self._rndis_configurator: |
- return AndroidRndisForwarder(self._device, self._rndis_configurator, |
- port_pairs) |
- return AndroidForwarder(self._device, port_pairs) |
- except Exception: |
- try: |
- logging.warning('Failed to create forwarder. ' |
- 'Currently forwarded connections:') |
- for line in self._device.adb.ForwardList().splitlines(): |
- logging.warning(' %s', line) |
- except Exception: |
- logging.warning('Exception raised while listing forwarded connections.') |
- |
- logging.warning('Device tcp sockets in use:') |
- try: |
- for line in self._device.ReadFile('/proc/net/tcp', as_root=True, |
- force_pull=True).splitlines(): |
- logging.warning(' %s', line) |
- except Exception: |
- logging.warning('Exception raised while listing tcp sockets.') |
- |
- logging.warning('Alive webpagereplay instances:') |
- try: |
- for line in subprocess.check_output(['ps', '-ef']).splitlines(): |
- if 'webpagereplay' in line: |
- logging.warning(' %s', line) |
- except Exception: |
- logging.warning('Exception raised while listing WPR intances.') |
- |
- raise |
- |
- @property |
- def host_ip(self): |
- if self._rndis_configurator: |
- return self._rndis_configurator.host_ip |
- return super(AndroidForwarderFactory, self).host_ip |
- |
- @property |
- def does_forwarder_override_dns(self): |
- return bool(self._rndis_configurator) |
- |
- |
-class AndroidForwarder(forwarders.Forwarder): |
- |
- def __init__(self, device, port_pairs): |
- super(AndroidForwarder, self).__init__(port_pairs) |
- self._device = device |
- forwarder.Forwarder.Map([(p.remote_port, p.local_port) |
- for p in port_pairs if p], self._device) |
- self._port_pairs = forwarders.PortPairs(*[ |
- forwarders.PortPair( |
- p.local_port, |
- forwarder.Forwarder.DevicePortForHostPort(p.local_port)) |
- if p else None for p in port_pairs]) |
- atexit.register(self.Close) |
- # TODO(tonyg): Verify that each port can connect to host. |
- |
- def Close(self): |
- if self._forwarding: |
- for port_pair in self._port_pairs: |
- if port_pair: |
- forwarder.Forwarder.UnmapDevicePort( |
- port_pair.remote_port, self._device) |
- super(AndroidForwarder, self).Close() |
- |
- |
-class AndroidRndisForwarder(forwarders.Forwarder): |
- """Forwards traffic using RNDIS. Assumes the device has root access.""" |
- |
- def __init__(self, device, rndis_configurator, port_pairs): |
- super(AndroidRndisForwarder, self).__init__(port_pairs) |
- |
- self._device = device |
- self._rndis_configurator = rndis_configurator |
- self._device_iface = rndis_configurator.device_iface |
- self._host_ip = rndis_configurator.host_ip |
- self._original_dns = None, None, None |
- self._RedirectPorts(port_pairs) |
- if port_pairs.dns: |
- self._OverrideDns() |
- self._OverrideDefaultGateway() |
- # Need to override routing policy again since call to setifdns |
- # sometimes resets policy table |
- self._rndis_configurator.OverrideRoutingPolicy() |
- atexit.register(self.Close) |
- # TODO(tonyg): Verify that each port can connect to host. |
- |
- @property |
- def host_ip(self): |
- return self._host_ip |
- |
- def Close(self): |
- if self._forwarding: |
- self._rndis_configurator.RestoreRoutingPolicy() |
- self._SetDns(*self._original_dns) |
- self._RestoreDefaultGateway() |
- super(AndroidRndisForwarder, self).Close() |
- |
- def _RedirectPorts(self, port_pairs): |
- """Sets the local to remote pair mappings to use for RNDIS.""" |
- # Flush any old nat rules. |
- self._device.RunShellCommand('iptables -F -t nat') |
- for port_pair in port_pairs: |
- if not port_pair or port_pair.local_port == port_pair.remote_port: |
- continue |
- protocol = 'udp' if port_pair.remote_port == 53 else 'tcp' |
- self._device.RunShellCommand( |
- 'iptables -t nat -A OUTPUT -p %s --dport %d' |
- ' -j DNAT --to-destination %s:%d' % |
- (protocol, port_pair.remote_port, self.host_ip, port_pair.local_port)) |
- |
- def _OverrideDns(self): |
- """Overrides DNS on device to point at the host.""" |
- self._original_dns = self._GetCurrentDns() |
- self._SetDns(self._device_iface, self.host_ip, self.host_ip) |
- |
- def _SetDns(self, iface, dns1, dns2): |
- """Overrides device's DNS configuration. |
- |
- Args: |
- iface: name of the network interface to make default |
- dns1, dns2: nameserver IP addresses |
- """ |
- if not iface: |
- return # If there is no route, then nobody cares about DNS. |
- # DNS proxy in older versions of Android is configured via properties. |
- # TODO(szym): run via su -c if necessary. |
- self._device.SetProp('net.dns1', dns1) |
- self._device.SetProp('net.dns2', dns2) |
- dnschange = self._device.GetProp('net.dnschange') |
- if dnschange: |
- self._device.SetProp('net.dnschange', str(int(dnschange) + 1)) |
- # Since commit 8b47b3601f82f299bb8c135af0639b72b67230e6 to frameworks/base |
- # the net.dns1 properties have been replaced with explicit commands for netd |
- self._device.RunShellCommand('netd resolver setifdns %s %s %s' % |
- (iface, dns1, dns2)) |
- # TODO(szym): if we know the package UID, we could setifaceforuidrange |
- self._device.RunShellCommand('netd resolver setdefaultif %s' % iface) |
- |
- def _GetCurrentDns(self): |
- """Returns current gateway, dns1, and dns2.""" |
- routes = self._device.RunShellCommand('cat /proc/net/route')[1:] |
- routes = [route.split() for route in routes] |
- default_routes = [route[0] for route in routes if route[1] == '00000000'] |
- return ( |
- default_routes[0] if default_routes else None, |
- self._device.GetProp('net.dns1'), |
- self._device.GetProp('net.dns2'), |
- ) |
- |
- def _OverrideDefaultGateway(self): |
- """Force traffic to go through RNDIS interface. |
- |
- Override any default gateway route. Without this traffic may go through |
- the wrong interface. |
- |
- This introduces the risk that _RestoreDefaultGateway() is not called |
- (e.g. Telemetry crashes). A power cycle or "adb reboot" is a simple |
- workaround around in that case. |
- """ |
- self._device.RunShellCommand('route add default gw %s dev %s' % |
- (self.host_ip, self._device_iface)) |
- |
- def _RestoreDefaultGateway(self): |
- self._device.RunShellCommand('netcfg %s down' % self._device_iface) |
- |
- |
-class AndroidRndisConfigurator(object): |
- """Configures a linux host to connect to an android device via RNDIS. |
- |
- Note that we intentionally leave RNDIS running on the device. This is |
- because the setup is slow and potentially flaky and leaving it running |
- doesn't seem to interfere with any other developer or bot use-cases. |
- """ |
- |
- _RNDIS_DEVICE = '/sys/class/android_usb/android0' |
- _NETWORK_INTERFACES = '/etc/network/interfaces' |
- _INTERFACES_INCLUDE = 'source /etc/network/interfaces.d/*.conf' |
- _TELEMETRY_INTERFACE_FILE = '/etc/network/interfaces.d/telemetry-{}.conf' |
- |
- def __init__(self, device): |
- self._device = device |
- |
- try: |
- self._device.EnableRoot() |
- except device_errors.CommandFailedError: |
- logging.error('RNDIS forwarding requires a rooted device.') |
- raise |
- |
- self._device_ip = None |
- self._host_iface = None |
- self._host_ip = None |
- self.device_iface = None |
- |
- if platform.GetHostPlatform().GetOSName() == 'mac': |
- self._InstallHorndis(platform.GetHostPlatform().GetArchName()) |
- |
- assert self._IsRndisSupported(), 'Device does not support RNDIS.' |
- self._CheckConfigureNetwork() |
- |
- @property |
- def host_ip(self): |
- return self._host_ip |
- |
- def _IsRndisSupported(self): |
- """Checks that the device has RNDIS support in the kernel.""" |
- return self._device.FileExists('%s/f_rndis/device' % self._RNDIS_DEVICE) |
- |
- def _FindDeviceRndisInterface(self): |
- """Returns the name of the RNDIS network interface if present.""" |
- config = self._device.RunShellCommand('ip -o link show') |
- interfaces = [line.split(':')[1].strip() for line in config] |
- candidates = [iface for iface in interfaces if re.match('rndis|usb', iface)] |
- if candidates: |
- candidates.sort() |
- if len(candidates) == 2 and candidates[0].startswith('rndis') and \ |
- candidates[1].startswith('usb'): |
- return candidates[0] |
- assert len(candidates) == 1, 'Found more than one rndis device!' |
- return candidates[0] |
- |
- def _EnumerateHostInterfaces(self): |
- host_platform = platform.GetHostPlatform().GetOSName() |
- if host_platform == 'linux': |
- return subprocess.check_output(['ip', 'addr']).splitlines() |
- if host_platform == 'mac': |
- return subprocess.check_output(['ifconfig']).splitlines() |
- raise NotImplementedError('Platform %s not supported!' % host_platform) |
- |
- def _FindHostRndisInterface(self): |
- """Returns the name of the host-side network interface.""" |
- interface_list = self._EnumerateHostInterfaces() |
- ether_address = self._device.ReadFile( |
- '%s/f_rndis/ethaddr' % self._RNDIS_DEVICE).strip() |
- interface_name = None |
- for line in interface_list: |
- if not line.startswith((' ', '\t')): |
- interface_name = line.split(':')[-2].strip() |
- elif ether_address in line: |
- return interface_name |
- |
- def _WriteProtectedFile(self, file_path, contents): |
- subprocess.check_call( |
- ['/usr/bin/sudo', 'bash', '-c', |
- 'echo -e "%s" > %s' % (contents, file_path)]) |
- |
- def _LoadInstalledHoRNDIS(self): |
- """Attempt to load HoRNDIS if installed. |
- If kext could not be loaded or if HoRNDIS is not installed, return False. |
- """ |
- if not os.path.isdir('/System/Library/Extensions/HoRNDIS.kext'): |
- logging.info('HoRNDIS not present on system.') |
- return False |
- |
- def HoRNDISLoaded(): |
- return 'HoRNDIS' in subprocess.check_output(['kextstat']) |
- |
- if HoRNDISLoaded(): |
- return True |
- |
- logging.info('HoRNDIS installed but not running, trying to load manually.') |
- subprocess.check_call( |
- ['/usr/bin/sudo', 'kextload', '-b', 'com.joshuawise.kexts.HoRNDIS']) |
- |
- return HoRNDISLoaded() |
- |
- def _InstallHorndis(self, arch_name): |
- if self._LoadInstalledHoRNDIS(): |
- logging.info('HoRNDIS kext loaded successfully.') |
- return |
- logging.info('Installing HoRNDIS...') |
- pkg_path = binary_manager.FetchPath('horndis', arch_name, 'mac') |
- subprocess.check_call( |
- ['/usr/bin/sudo', 'installer', '-pkg', pkg_path, '-target', '/']) |
- |
- def _DisableRndis(self): |
- try: |
- self._device.SetProp('sys.usb.config', 'adb') |
- except device_errors.AdbCommandFailedError: |
- # Ignore exception due to USB connection being reset. |
- pass |
- self._device.WaitUntilFullyBooted() |
- |
- def _EnableRndis(self): |
- """Enables the RNDIS network interface.""" |
- script_prefix = '/data/local/tmp/rndis' |
- # This could be accomplished via "svc usb setFunction rndis" but only on |
- # devices which have the "USB tethering" feature. |
- # Also, on some devices, it's necessary to go through "none" function. |
- script = """ |
-trap '' HUP |
-trap '' TERM |
-trap '' PIPE |
- |
-function manual_config() { |
- echo %(functions)s > %(dev)s/functions |
- echo 224 > %(dev)s/bDeviceClass |
- echo 1 > %(dev)s/enable |
- start adbd |
- setprop sys.usb.state %(functions)s |
-} |
- |
-# This function kills adb transport, so it has to be run "detached". |
-function doit() { |
- setprop sys.usb.config none |
- while [ `getprop sys.usb.state` != "none" ]; do |
- sleep 1 |
- done |
- manual_config |
- # For some combinations of devices and host kernels, adb won't work unless the |
- # interface is up, but if we bring it up immediately, it will break adb. |
- #sleep 1 |
- #ifconfig rndis0 192.168.123.2 netmask 255.255.255.0 up |
- echo DONE >> %(prefix)s.log |
-} |
- |
-doit & |
- """ % {'dev': self._RNDIS_DEVICE, 'functions': 'rndis,adb', |
- 'prefix': script_prefix} |
- self._device.WriteFile('%s.sh' % script_prefix, script) |
- # TODO(szym): run via su -c if necessary. |
- self._device.RunShellCommand('rm %s.log' % script_prefix) |
- self._device.RunShellCommand('. %s.sh' % script_prefix) |
- self._device.WaitUntilFullyBooted() |
- result = self._device.ReadFile('%s.log' % script_prefix).splitlines() |
- assert any('DONE' in line for line in result), 'RNDIS script did not run!' |
- |
- def _CheckEnableRndis(self, force): |
- """Enables the RNDIS network interface, retrying if necessary. |
- Args: |
- force: Disable RNDIS first, even if it appears already enabled. |
- Returns: |
- device_iface: RNDIS interface name on the device |
- host_iface: corresponding interface name on the host |
- """ |
- for _ in range(3): |
- if not force: |
- device_iface = self._FindDeviceRndisInterface() |
- if device_iface: |
- host_iface = self._FindHostRndisInterface() |
- if host_iface: |
- return device_iface, host_iface |
- self._DisableRndis() |
- self._EnableRndis() |
- force = False |
- raise Exception('Could not enable RNDIS, giving up.') |
- |
- def _Ip2Long(self, addr): |
- return struct.unpack('!L', socket.inet_aton(addr))[0] |
- |
- def _IpPrefix2AddressMask(self, addr): |
- def _Length2Mask(length): |
- return 0xFFFFFFFF & ~((1 << (32 - length)) - 1) |
- |
- addr, masklen = addr.split('/') |
- return self._Ip2Long(addr), _Length2Mask(int(masklen)) |
- |
- def _GetHostAddresses(self, iface): |
- """Returns the IP addresses on host's interfaces, breaking out |iface|.""" |
- interface_list = self._EnumerateHostInterfaces() |
- addresses = [] |
- iface_address = None |
- found_iface = False |
- for line in interface_list: |
- if not line.startswith((' ', '\t')): |
- found_iface = iface in line |
- match = re.search(r'(?<=inet )\S+', line) |
- if match: |
- address = match.group(0) |
- if '/' in address: |
- address = self._IpPrefix2AddressMask(address) |
- else: |
- match = re.search(r'(?<=netmask )\S+', line) |
- address = self._Ip2Long(address), int(match.group(0), 16) |
- if found_iface: |
- assert not iface_address, ( |
- 'Found %s twice when parsing host interfaces.' % iface) |
- iface_address = address |
- else: |
- addresses.append(address) |
- return addresses, iface_address |
- |
- def _GetDeviceAddresses(self, excluded_iface): |
- """Returns the IP addresses on all connected devices. |
- Excludes interface |excluded_iface| on the selected device. |
- """ |
- my_device = str(self._device) |
- addresses = [] |
- for device_serial in android_device.GetDeviceSerials(None): |
- try: |
- device = device_utils.DeviceUtils(device_serial) |
- if device_serial == my_device: |
- excluded = excluded_iface |
- else: |
- excluded = 'no interfaces excluded on other devices' |
- addresses += [line.split()[3] |
- for line in device.RunShellCommand('ip -o -4 addr') |
- if excluded not in line] |
- except device_errors.CommandFailedError: |
- logging.warning('Unable to determine IP addresses for %s', |
- device_serial) |
- return addresses |
- |
- def _ConfigureNetwork(self, device_iface, host_iface): |
- """Configures the |device_iface| to be on the same network as |host_iface|. |
- """ |
- def _Long2Ip(value): |
- return socket.inet_ntoa(struct.pack('!L', value)) |
- |
- def _IsNetworkUnique(network, addresses): |
- return all((addr & mask != network & mask) for addr, mask in addresses) |
- |
- def _NextUnusedAddress(network, netmask, used_addresses): |
- # Excludes '0' and broadcast. |
- for suffix in range(1, 0xFFFFFFFF & ~netmask): |
- candidate = network | suffix |
- if candidate not in used_addresses: |
- return candidate |
- |
- def HasHostAddress(): |
- _, host_address = self._GetHostAddresses(host_iface) |
- return bool(host_address) |
- |
- if not HasHostAddress(): |
- if platform.GetHostPlatform().GetOSName() == 'mac': |
- if 'Telemetry' not in subprocess.check_output( |
- ['networksetup', '-listallnetworkservices']): |
- subprocess.check_call( |
- ['/usr/bin/sudo', 'networksetup', |
- '-createnetworkservice', 'Telemetry', host_iface]) |
- subprocess.check_call( |
- ['/usr/bin/sudo', 'networksetup', |
- '-setmanual', 'Telemetry', '192.168.123.1', '255.255.255.0']) |
- elif platform.GetHostPlatform().GetOSName() == 'linux': |
- with open(self._NETWORK_INTERFACES) as f: |
- orig_interfaces = f.read() |
- if self._INTERFACES_INCLUDE not in orig_interfaces: |
- interfaces = '\n'.join([ |
- orig_interfaces, |
- '', |
- '# Added by Telemetry.', |
- self._INTERFACES_INCLUDE]) |
- self._WriteProtectedFile(self._NETWORK_INTERFACES, interfaces) |
- interface_conf_file = self._TELEMETRY_INTERFACE_FILE.format(host_iface) |
- if not os.path.exists(interface_conf_file): |
- interface_conf_dir = os.path.dirname(interface_conf_file) |
- if not os.path.exists(interface_conf_dir): |
- subprocess.call(['/usr/bin/sudo', '/bin/mkdir', interface_conf_dir]) |
- subprocess.call( |
- ['/usr/bin/sudo', '/bin/chmod', '755', interface_conf_dir]) |
- interface_conf = '\n'.join([ |
- '# Added by Telemetry for RNDIS forwarding.', |
- 'allow-hotplug %s' % host_iface, |
- 'iface %s inet static' % host_iface, |
- ' address 192.168.123.1', |
- ' netmask 255.255.255.0', |
- ]) |
- self._WriteProtectedFile(interface_conf_file, interface_conf) |
- subprocess.check_call(['/usr/bin/sudo', 'ifup', host_iface]) |
- logging.info('Waiting for RNDIS connectivity...') |
- util.WaitFor(HasHostAddress, 30) |
- |
- addresses, host_address = self._GetHostAddresses(host_iface) |
- assert host_address, 'Interface %s could not be configured.' % host_iface |
- |
- host_ip, netmask = host_address # pylint: disable=unpacking-non-sequence |
- network = host_ip & netmask |
- |
- if not _IsNetworkUnique(network, addresses): |
- logging.warning( |
- 'The IP address configuration %s of %s is not unique!\n' |
- 'Check your /etc/network/interfaces. If this overlap is intended,\n' |
- 'you might need to use: ip rule add from <device_ip> lookup <table>\n' |
- 'or add the interface to a bridge in order to route to this network.' |
- % (host_address, host_iface) |
- ) |
- |
- # Find unused IP address. |
- used_addresses = [addr for addr, _ in addresses] |
- used_addresses += [self._IpPrefix2AddressMask(addr)[0] |
- for addr in self._GetDeviceAddresses(device_iface)] |
- used_addresses += [host_ip] |
- |
- device_ip = _NextUnusedAddress(network, netmask, used_addresses) |
- assert device_ip, ('The network %s on %s is full.' % |
- (host_address, host_iface)) |
- |
- host_ip = _Long2Ip(host_ip) |
- device_ip = _Long2Ip(device_ip) |
- netmask = _Long2Ip(netmask) |
- |
- # TODO(szym) run via su -c if necessary. |
- self._device.RunShellCommand( |
- 'ifconfig %s %s netmask %s up' % (device_iface, device_ip, netmask)) |
- # Enabling the interface sometimes breaks adb. |
- self._device.WaitUntilFullyBooted() |
- self._host_iface = host_iface |
- self._host_ip = host_ip |
- self.device_iface = device_iface |
- self._device_ip = device_ip |
- |
- def _TestConnectivity(self): |
- with open(os.devnull, 'wb') as devnull: |
- return subprocess.call(['ping', '-q', '-c1', '-W1', self._device_ip], |
- stdout=devnull) == 0 |
- |
- def OverrideRoutingPolicy(self): |
- """Override any routing policy that could prevent |
- packets from reaching the rndis interface |
- """ |
- policies = self._device.RunShellCommand('ip rule') |
- if len(policies) > 1 and not 'lookup main' in policies[1]: |
- self._device.RunShellCommand('ip rule add prio 1 from all table main') |
- self._device.RunShellCommand('ip route flush cache') |
- |
- def RestoreRoutingPolicy(self): |
- policies = self._device.RunShellCommand('ip rule') |
- if len(policies) > 1 and re.match("^1:.*lookup main", policies[1]): |
- self._device.RunShellCommand('ip rule del prio 1') |
- self._device.RunShellCommand('ip route flush cache') |
- |
- def _CheckConfigureNetwork(self): |
- """Enables RNDIS and configures it, retrying until we have connectivity.""" |
- force = False |
- for _ in range(3): |
- device_iface, host_iface = self._CheckEnableRndis(force) |
- self._ConfigureNetwork(device_iface, host_iface) |
- self.OverrideRoutingPolicy() |
- # Sometimes the first packet will wake up the connection. |
- for _ in range(3): |
- if self._TestConnectivity(): |
- return |
- force = True |
- self.RestoreRoutingPolicy() |
- raise Exception('No connectivity, giving up.') |