Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(181)

Unified Diff: tools/telemetry/telemetry/core/chrome/android_rndis.py

Issue 22286010: [telemetry] Implement Forwarder using RNDIS. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: simpler darwin check Created 7 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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)
« no previous file with comments | « tools/telemetry/telemetry/core/chrome/android_browser_backend.py ('k') | tools/telemetry/telemetry/core/wpr_server.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698