• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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