Index: tools/telemetry/telemetry/core/chrome/android_rndis.py |
diff --git a/tools/telemetry/telemetry/core/chrome/android_rndis.py b/tools/telemetry/telemetry/core/chrome/android_rndis.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..eac938c49bc6a465f40be791bee33870fce2da85 |
--- /dev/null |
+++ b/tools/telemetry/telemetry/core/chrome/android_rndis.py |
@@ -0,0 +1,339 @@ |
+# Copyright (c) 2013 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 logging |
+import os |
+import re |
+import socket |
+import struct |
+import subprocess |
+import sys |
+ |
+from telemetry.core.chrome import adb_commands |
+ |
bulach
2013/09/02 08:49:01
nit: here and 27 below, two \n between top-levels
|
+def _CheckOutput(*popenargs, **kwargs): |
+ """Backport of subprocess.check_output to python 2.6""" |
+ process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs) |
+ output, _ = process.communicate() |
+ retcode = process.poll() |
+ if retcode: |
+ cmd = kwargs.get("args") |
bulach
2013/09/02 08:49:01
nit: ' is more common than "
|
+ if cmd is None: |
+ cmd = popenargs[0] |
+ error = subprocess.CalledProcessError(retcode, cmd) |
+ error.output = output |
+ raise error |
+ return output |
+ |
+class RndisForwarderWithRoot(object): |
+ """Forwards traffic using RNDIS. Assuming the device has root access. |
+ """ |
+ _RNDIS_DEVICE = '/sys/class/android_usb/android0' |
+ |
+ def __init__(self, adb): |
+ """Args: |
+ adb: an instance of AdbCommands |
+ """ |
+ assert adb.IsRootEnabled(), 'Root must be enabled to use RNDIS forwarding.' |
+ self._adb = adb.Adb() |
+ |
+ self._host_port = 80 |
+ self._host_ip = None |
+ self._device_ip = None |
+ self._host_iface = None |
+ self._device_iface = None |
+ self._original_dns = None, None, None |
+ |
+ assert self._IsRndisSupported(), 'Device does not have rndis!' |
+ self._CheckConfigureNetwork() |
+ |
+ def SetPorts(self, *port_pairs): |
+ """Args: |
+ port_pairs: Used for compatibility with Forwarder. RNDIS does not |
+ support mapping so local_port must match remote_port in all pairs. |
+ """ |
+ assert all(pair.remote_port == pair.local_port for pair in port_pairs), \ |
+ 'Local and remote ports must be the same on all pairs with RNDIS.' |
+ self._host_port = port_pairs[0].local_port |
+ |
+ def OverrideDns(self): |
+ """Overrides DNS on device to point at the host.""" |
+ self._original_dns = self._GetCurrentDns() |
+ self._OverrideDns(self._device_iface, self._host_ip, self._host_ip) |
+ |
+ def _IsRndisSupported(self): |
+ """Checks that the device has RNDIS support in the kernel.""" |
+ return self._adb.FileExistsOnDevice( |
+ '%s/f_rndis/device' % self._RNDIS_DEVICE) |
+ |
+ def _WaitForDevice(self): |
+ self._adb.Adb().SendCommand('wait-for-device') |
+ |
+ def _FindDeviceRndisInterface(self): |
+ """Returns the name of the RNDIS network interface if present.""" |
+ config = self._adb.RunShellCommand('netcfg') |
+ candidates = [line.split()[0] for line in config if 'rndis' in line] |
+ if candidates: |
+ assert len(candidates) == 1, 'Found more than one rndis device!' |
+ return candidates[0] |
+ |
+ def _EnumerateHostInterfaces(self): |
+ if sys.platform.startswith('linux'): |
+ return _CheckOutput(['ip', 'addr']).splitlines() |
+ elif sys.platform == 'darwin': |
+ return _CheckOutput(['ifconfig']).splitlines() |
+ raise Exception('Platform %s not supported!' % sys.platform) |
+ |
+ def _FindHostRndisInterface(self): |
+ """Returns the name of the host-side network interface.""" |
+ interface_list = self._EnumerateHostInterfaces() |
+ ether_address = self._adb.GetFileContents( |
+ '%s/f_rndis/ethaddr' % self._RNDIS_DEVICE)[0] |
+ interface_name = None |
+ for line in interface_list: |
+ if not line.startswith(' '): |
+ interface_name = line.split()[1].strip(':') |
+ elif ether_address in line: |
+ return interface_name |
+ |
+ def _DisableRndis(self): |
+ self._adb.RunShellCommand('setprop sys.usb.config adb') |
+ self._WaitForDevice() |
+ |
+ 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.42.2 netmask 255.255.255.0 up |
+ echo DONE >> %(prefix)s.log |
+} |
+ |
+doit & |
+ """ % {'dev': self._RNDIS_DEVICE, 'functions': 'rndis,adb', |
+ 'prefix': script_prefix } |
+ self._adb.SetFileContents('%s.sh' % script_prefix, script) |
+ # TODO(szym): run via su -c if necessary. |
+ self._adb.RunShellCommand('rm %s.log' % script_prefix) |
+ self._adb.RunShellCommand('. %s.sh' % script_prefix) |
+ self._WaitForDevice() |
+ result = self._adb.GetFileContents('%s.log' % script_prefix) |
+ 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 _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(' '): |
+ found_iface = iface in line |
+ match = re.search('(?<=inet )\S+', line) |
+ if match: |
+ address = match.group(0) |
+ 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 = self._adb.GetDevice() |
+ addresses = [] |
+ for device in adb_commands.GetAttachedDevices(): |
+ adb = adb_commands.AdbCommands(device).Adb() |
+ if device == my_device: |
+ excluded = excluded_iface |
+ else: |
+ excluded = 'no interfaces excluded on other devices' |
+ addresses += [line.split()[2] for line in adb.RunShellCommand('netcfg') |
+ if excluded not in line] |
+ return addresses |
+ |
+ def _ConfigureNetwork(self, device_iface, host_iface): |
+ """Configures the |device_iface| to be on the same network as |host_iface|. |
+ """ |
+ def _Ip2Long(addr): |
+ return struct.unpack('!L', socket.inet_aton(addr))[0] |
+ |
+ def _Long2Ip(value): |
+ return socket.inet_ntoa(struct.pack('!L', value)) |
+ |
+ def _Length2Mask(length): |
+ return 0xFFFFFFFF & ~((1 << (32 - length)) - 1) |
+ |
+ def _IpPrefix2AddressMask(addr): |
+ addr, masklen = addr.split('/') |
+ return _Ip2Long(addr), _Length2Mask(int(masklen)) |
+ |
+ 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 |
+ |
+ addresses, host_address = self._GetHostAddresses(host_iface) |
+ |
+ assert host_address, ('Interface %(iface)s was not configured.\n' |
+ 'To configure it automatically, add to /etc/network/interfaces:\n' |
+ 'auto %(iface)s\n' |
+ 'iface %(iface)s\n' |
+ ' address 192.168.<unique>.1\n' |
+ ' netmask 255.255.255.0' % {'iface': host_iface}) |
+ |
+ addresses = [_IpPrefix2AddressMask(addr) for addr in addresses] |
+ host_ip, netmask = _IpPrefix2AddressMask(host_address) |
+ |
+ 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 += [_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._adb.RunShellCommand('ifconfig %s %s netmask %s up' % |
+ (device_iface, device_ip, netmask)) |
+ # Enabling the interface sometimes breaks adb. |
+ self._WaitForDevice() |
+ 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', |
+ '-I', self._host_iface, self._device_ip], |
+ stdout=devnull) == 0 |
+ |
+ 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) |
+ if self._TestConnectivity(): |
+ return |
+ force = True |
+ raise Exception('No connectivity, giving up.') |
+ |
+ def _GetCurrentDns(self): |
+ """Returns current gateway, dns1, and dns2.""" |
+ routes = self._adb.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._adb.RunShellCommand('getprop net.dns1')[0], |
+ self._adb.RunShellCommand('getprop net.dns2')[0], |
+ ) |
+ |
+ def _OverrideDns(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._adb.RunShellCommand('setprop net.dns1 ' + dns1) |
+ self._adb.RunShellCommand('setprop net.dns2 ' + dns2) |
+ dnschange = self._adb.RunShellCommand('getprop net.dnschange')[0] |
+ if dnschange: |
+ self._adb.RunShellCommand('setprop net.dnschange %s' % |
+ (int(dnschange) + 1)) |
+ # Since commit 8b47b3601f82f299bb8c135af0639b72b67230e6 to frameworks/base |
+ # the net.dns1 properties have been replaced with explicit commands for netd |
+ self._adb.RunShellCommand('ndc netd resolver setifdns %s %s %s' % |
+ (iface, dns1, dns2)) |
+ # TODO(szym): if we know the package UID, we could setifaceforuidrange |
+ self._adb.RunShellCommand('ndc netd resolver setdefaultif %s' % iface) |
+ |
+ @property |
+ def host_ip(self): |
+ return self._host_ip |
+ |
+ @property |
+ def url(self): |
+ # localhost and domains which resolve on the host's private network will not |
+ # be resolved by the DNS proxy to the HTTP proxy. |
+ return 'http://%s:%d' % (self._host_ip, self._host_port) |
+ |
+ def Close(self): |
+ self._OverrideDns(*self._original_dns) |