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

Side by Side Diff: components/cronet/android/test/javaperftests/android_rndis_forwarder.py

Issue 1942283002: [Cronet] Resurrect Android USB reverse tethering (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Created 4 years, 7 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 unified diff | Download patch
« no previous file with comments | « no previous file | components/cronet/android/test/javaperftests/run.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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.')
OLDNEW
« no previous file with comments | « no previous file | components/cronet/android/test/javaperftests/run.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698