1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.cts.net.hostside; 18 19 import static android.system.OsConstants.*; 20 21 import android.content.Intent; 22 import android.content.pm.PackageManager; 23 import android.net.ConnectivityManager; 24 import android.net.ConnectivityManager.NetworkCallback; 25 import android.net.LinkProperties; 26 import android.net.Network; 27 import android.net.NetworkCapabilities; 28 import android.net.NetworkRequest; 29 import android.net.VpnService; 30 import android.support.test.uiautomator.UiDevice; 31 import android.support.test.uiautomator.UiObject; 32 import android.support.test.uiautomator.UiObjectNotFoundException; 33 import android.support.test.uiautomator.UiScrollable; 34 import android.support.test.uiautomator.UiSelector; 35 import android.system.ErrnoException; 36 import android.system.Os; 37 import android.system.StructPollfd; 38 import android.test.InstrumentationTestCase; 39 import android.test.MoreAsserts; 40 import android.text.TextUtils; 41 import android.util.Log; 42 43 import java.io.Closeable; 44 import java.io.FileDescriptor; 45 import java.io.IOException; 46 import java.io.InputStream; 47 import java.io.OutputStream; 48 import java.net.DatagramPacket; 49 import java.net.DatagramSocket; 50 import java.net.Inet6Address; 51 import java.net.InetAddress; 52 import java.net.InetSocketAddress; 53 import java.net.ServerSocket; 54 import java.net.Socket; 55 import java.util.Random; 56 57 /** 58 * Tests for the VpnService API. 59 * 60 * These tests establish a VPN via the VpnService API, and have the service reflect the packets back 61 * to the device without causing any network traffic. This allows testing the local VPN data path 62 * without a network connection or a VPN server. 63 * 64 * Note: in Lollipop, VPN functionality relies on kernel support for UID-based routing. If these 65 * tests fail, it may be due to the lack of kernel support. The necessary patches can be 66 * cherry-picked from the Android common kernel trees: 67 * 68 * android-3.10: 69 * https://android-review.googlesource.com/#/c/99220/ 70 * https://android-review.googlesource.com/#/c/100545/ 71 * 72 * android-3.4: 73 * https://android-review.googlesource.com/#/c/99225/ 74 * https://android-review.googlesource.com/#/c/100557/ 75 * 76 */ 77 public class VpnTest extends InstrumentationTestCase { 78 79 public static String TAG = "VpnTest"; 80 public static int TIMEOUT_MS = 3 * 1000; 81 public static int SOCKET_TIMEOUT_MS = 100; 82 83 private UiDevice mDevice; 84 private MyActivity mActivity; 85 private String mPackageName; 86 private ConnectivityManager mCM; 87 Network mNetwork; 88 NetworkCallback mCallback; 89 final Object mLock = new Object(); 90 final Object mLockShutdown = new Object(); 91 supportedHardware()92 private boolean supportedHardware() { 93 final PackageManager pm = getInstrumentation().getContext().getPackageManager(); 94 return !pm.hasSystemFeature("android.hardware.type.television") && 95 !pm.hasSystemFeature("android.hardware.type.watch"); 96 } 97 98 @Override setUp()99 public void setUp() throws Exception { 100 super.setUp(); 101 102 mNetwork = null; 103 mCallback = null; 104 105 mDevice = UiDevice.getInstance(getInstrumentation()); 106 mActivity = launchActivity(getInstrumentation().getTargetContext().getPackageName(), 107 MyActivity.class, null); 108 mPackageName = mActivity.getPackageName(); 109 mCM = (ConnectivityManager) mActivity.getSystemService(mActivity.CONNECTIVITY_SERVICE); 110 mDevice.waitForIdle(); 111 } 112 113 @Override tearDown()114 public void tearDown() throws Exception { 115 if (mCallback != null) { 116 mCM.unregisterNetworkCallback(mCallback); 117 } 118 Log.i(TAG, "Stopping VPN"); 119 stopVpn(); 120 mActivity.finish(); 121 super.tearDown(); 122 } 123 prepareVpn()124 private void prepareVpn() throws Exception { 125 final int REQUEST_ID = 42; 126 127 // Attempt to prepare. 128 Log.i(TAG, "Preparing VPN"); 129 Intent intent = VpnService.prepare(mActivity); 130 131 if (intent != null) { 132 // Start the confirmation dialog and click OK. 133 mActivity.startActivityForResult(intent, REQUEST_ID); 134 mDevice.waitForIdle(); 135 136 String packageName = intent.getComponent().getPackageName(); 137 String resourceIdRegex = "android:id/button1$|button_start_vpn"; 138 final UiObject okButton = new UiObject(new UiSelector() 139 .className("android.widget.Button") 140 .packageName(packageName) 141 .resourceIdMatches(resourceIdRegex)); 142 if (okButton.waitForExists(TIMEOUT_MS) == false) { 143 mActivity.finishActivity(REQUEST_ID); 144 fail("VpnService.prepare returned an Intent for '" + intent.getComponent() + "' " + 145 "to display the VPN confirmation dialog, but this test could not find the " + 146 "button to allow the VPN application to connect. Please ensure that the " + 147 "component displays a button with a resource ID matching the regexp: '" + 148 resourceIdRegex + "'."); 149 } 150 151 // Click the button and wait for RESULT_OK. 152 okButton.click(); 153 try { 154 int result = mActivity.getResult(TIMEOUT_MS); 155 if (result != MyActivity.RESULT_OK) { 156 fail("The VPN confirmation dialog did not return RESULT_OK when clicking on " + 157 "the button matching the regular expression '" + resourceIdRegex + 158 "' of " + intent.getComponent() + "'. Please ensure that clicking on " + 159 "that button allows the VPN application to connect. " + 160 "Return value: " + result); 161 } 162 } catch (InterruptedException e) { 163 fail("VPN confirmation dialog did not return after " + TIMEOUT_MS + "ms"); 164 } 165 166 // Now we should be prepared. 167 intent = VpnService.prepare(mActivity); 168 if (intent != null) { 169 fail("VpnService.prepare returned non-null even after the VPN dialog " + 170 intent.getComponent() + "returned RESULT_OK."); 171 } 172 } 173 } 174 startVpn( String[] addresses, String[] routes, String allowedApplications, String disallowedApplications)175 private void startVpn( 176 String[] addresses, String[] routes, 177 String allowedApplications, String disallowedApplications) throws Exception { 178 179 prepareVpn(); 180 181 // Register a callback so we will be notified when our VPN comes up. 182 final NetworkRequest request = new NetworkRequest.Builder() 183 .addTransportType(NetworkCapabilities.TRANSPORT_VPN) 184 .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) 185 .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) 186 .build(); 187 mCallback = new NetworkCallback() { 188 public void onAvailable(Network network) { 189 synchronized (mLock) { 190 Log.i(TAG, "Got available callback for network=" + network); 191 mNetwork = network; 192 mLock.notify(); 193 } 194 } 195 }; 196 mCM.registerNetworkCallback(request, mCallback); // Unregistered in tearDown. 197 198 // Start the service and wait up for TIMEOUT_MS ms for the VPN to come up. 199 Intent intent = new Intent(mActivity, MyVpnService.class) 200 .putExtra(mPackageName + ".cmd", "connect") 201 .putExtra(mPackageName + ".addresses", TextUtils.join(",", addresses)) 202 .putExtra(mPackageName + ".routes", TextUtils.join(",", routes)) 203 .putExtra(mPackageName + ".allowedapplications", allowedApplications) 204 .putExtra(mPackageName + ".disallowedapplications", disallowedApplications); 205 mActivity.startService(intent); 206 synchronized (mLock) { 207 if (mNetwork == null) { 208 Log.i(TAG, "bf mLock"); 209 mLock.wait(TIMEOUT_MS); 210 Log.i(TAG, "af mLock"); 211 } 212 } 213 214 if (mNetwork == null) { 215 fail("VPN did not become available after " + TIMEOUT_MS + "ms"); 216 } 217 218 // Unfortunately, when the available callback fires, the VPN UID ranges are not yet 219 // configured. Give the system some time to do so. http://b/18436087 . 220 try { Thread.sleep(3000); } catch(InterruptedException e) {} 221 } 222 stopVpn()223 private void stopVpn() { 224 // Register a callback so we will be notified when our VPN comes up. 225 final NetworkRequest request = new NetworkRequest.Builder() 226 .addTransportType(NetworkCapabilities.TRANSPORT_VPN) 227 .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) 228 .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) 229 .build(); 230 mCallback = new NetworkCallback() { 231 public void onLost(Network network) { 232 synchronized (mLockShutdown) { 233 Log.i(TAG, "Got lost callback for network=" + network + ",mNetwork = " + mNetwork); 234 if( mNetwork == network){ 235 mLockShutdown.notify(); 236 } 237 } 238 } 239 }; 240 mCM.registerNetworkCallback(request, mCallback); // Unregistered in tearDown. 241 // Simply calling mActivity.stopService() won't stop the service, because the system binds 242 // to the service for the purpose of sending it a revoke command if another VPN comes up, 243 // and stopping a bound service has no effect. Instead, "start" the service again with an 244 // Intent that tells it to disconnect. 245 Intent intent = new Intent(mActivity, MyVpnService.class) 246 .putExtra(mPackageName + ".cmd", "disconnect"); 247 mActivity.startService(intent); 248 synchronized (mLockShutdown) { 249 try { 250 Log.i(TAG, "bf mLockShutdown"); 251 mLockShutdown.wait(TIMEOUT_MS); 252 Log.i(TAG, "af mLockShutdown"); 253 } catch(InterruptedException e) {} 254 } 255 } 256 closeQuietly(Closeable c)257 private static void closeQuietly(Closeable c) { 258 if (c != null) { 259 try { 260 c.close(); 261 } catch (IOException e) { 262 } 263 } 264 } 265 checkPing(String to)266 private static void checkPing(String to) throws IOException, ErrnoException { 267 InetAddress address = InetAddress.getByName(to); 268 FileDescriptor s; 269 final int LENGTH = 64; 270 byte[] packet = new byte[LENGTH]; 271 byte[] header; 272 273 // Construct a ping packet. 274 Random random = new Random(); 275 random.nextBytes(packet); 276 if (address instanceof Inet6Address) { 277 s = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6); 278 header = new byte[] { (byte) 0x80, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; 279 } else { 280 // Note that this doesn't actually work due to http://b/18558481 . 281 s = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP); 282 header = new byte[] { (byte) 0x08, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; 283 } 284 System.arraycopy(header, 0, packet, 0, header.length); 285 286 // Send the packet. 287 int port = random.nextInt(65534) + 1; 288 Os.connect(s, address, port); 289 Os.write(s, packet, 0, packet.length); 290 291 // Expect a reply. 292 StructPollfd pollfd = new StructPollfd(); 293 pollfd.events = (short) POLLIN; // "error: possible loss of precision" 294 pollfd.fd = s; 295 int ret = Os.poll(new StructPollfd[] { pollfd }, SOCKET_TIMEOUT_MS); 296 assertEquals("Expected reply after sending ping", 1, ret); 297 298 byte[] reply = new byte[LENGTH]; 299 int read = Os.read(s, reply, 0, LENGTH); 300 assertEquals(LENGTH, read); 301 302 // Find out what the kernel set the ICMP ID to. 303 InetSocketAddress local = (InetSocketAddress) Os.getsockname(s); 304 port = local.getPort(); 305 packet[4] = (byte) ((port >> 8) & 0xff); 306 packet[5] = (byte) (port & 0xff); 307 308 // Check the contents. 309 if (packet[0] == (byte) 0x80) { 310 packet[0] = (byte) 0x81; 311 } else { 312 packet[0] = 0; 313 } 314 // Zero out the checksum in the reply so it matches the uninitialized checksum in packet. 315 reply[2] = reply[3] = 0; 316 MoreAsserts.assertEquals(packet, reply); 317 } 318 319 // Writes data to out and checks that it appears identically on in. writeAndCheckData( OutputStream out, InputStream in, byte[] data)320 private static void writeAndCheckData( 321 OutputStream out, InputStream in, byte[] data) throws IOException { 322 out.write(data, 0, data.length); 323 out.flush(); 324 325 byte[] read = new byte[data.length]; 326 int bytesRead = 0, totalRead = 0; 327 do { 328 bytesRead = in.read(read, totalRead, read.length - totalRead); 329 totalRead += bytesRead; 330 } while (bytesRead >= 0 && totalRead < data.length); 331 assertEquals(totalRead, data.length); 332 MoreAsserts.assertEquals(data, read); 333 } 334 checkTcpReflection(String to, String expectedFrom)335 private static void checkTcpReflection(String to, String expectedFrom) throws IOException { 336 // Exercise TCP over the VPN by "connecting to ourselves". We open a server socket and a 337 // client socket, and connect the client socket to a remote host, with the port of the 338 // server socket. The PacketReflector reflects the packets, changing the source addresses 339 // but not the ports, so our client socket is connected to our server socket, though both 340 // sockets think their peers are on the "remote" IP address. 341 342 // Open a listening socket. 343 ServerSocket listen = new ServerSocket(0, 10, InetAddress.getByName("::")); 344 345 // Connect the client socket to it. 346 InetAddress toAddr = InetAddress.getByName(to); 347 Socket client = new Socket(); 348 try { 349 client.connect(new InetSocketAddress(toAddr, listen.getLocalPort()), SOCKET_TIMEOUT_MS); 350 if (expectedFrom == null) { 351 closeQuietly(listen); 352 closeQuietly(client); 353 fail("Expected connection to fail, but it succeeded."); 354 } 355 } catch (IOException e) { 356 if (expectedFrom != null) { 357 closeQuietly(listen); 358 fail("Expected connection to succeed, but it failed."); 359 } else { 360 // We expected the connection to fail, and it did, so there's nothing more to test. 361 return; 362 } 363 } 364 365 // The connection succeeded, and we expected it to succeed. Send some data; if things are 366 // working, the data will be sent to the VPN, reflected by the PacketReflector, and arrive 367 // at our server socket. For good measure, send some data in the other direction. 368 Socket server = null; 369 try { 370 // Accept the connection on the server side. 371 listen.setSoTimeout(SOCKET_TIMEOUT_MS); 372 server = listen.accept(); 373 374 // Check that the source and peer addresses are as expected. 375 assertEquals(expectedFrom, client.getLocalAddress().getHostAddress()); 376 assertEquals(expectedFrom, server.getLocalAddress().getHostAddress()); 377 assertEquals( 378 new InetSocketAddress(toAddr, client.getLocalPort()), 379 server.getRemoteSocketAddress()); 380 assertEquals( 381 new InetSocketAddress(toAddr, server.getLocalPort()), 382 client.getRemoteSocketAddress()); 383 384 // Now write some data. 385 final int LENGTH = 32768; 386 byte[] data = new byte[LENGTH]; 387 new Random().nextBytes(data); 388 389 // Make sure our writes don't block or time out, because we're single-threaded and can't 390 // read and write at the same time. 391 server.setReceiveBufferSize(LENGTH * 2); 392 client.setSendBufferSize(LENGTH * 2); 393 client.setSoTimeout(SOCKET_TIMEOUT_MS); 394 server.setSoTimeout(SOCKET_TIMEOUT_MS); 395 396 // Send some data from client to server, then from server to client. 397 writeAndCheckData(client.getOutputStream(), server.getInputStream(), data); 398 writeAndCheckData(server.getOutputStream(), client.getInputStream(), data); 399 } finally { 400 closeQuietly(listen); 401 closeQuietly(client); 402 closeQuietly(server); 403 } 404 } 405 checkUdpEcho(String to, String expectedFrom)406 private static void checkUdpEcho(String to, String expectedFrom) throws IOException { 407 DatagramSocket s; 408 InetAddress address = InetAddress.getByName(to); 409 if (address instanceof Inet6Address) { // http://b/18094870 410 s = new DatagramSocket(0, InetAddress.getByName("::")); 411 } else { 412 s = new DatagramSocket(); 413 } 414 s.setSoTimeout(SOCKET_TIMEOUT_MS); 415 416 Random random = new Random(); 417 byte[] data = new byte[random.nextInt(1650)]; 418 random.nextBytes(data); 419 DatagramPacket p = new DatagramPacket(data, data.length); 420 s.connect(address, 7); 421 422 if (expectedFrom != null) { 423 assertEquals("Unexpected source address: ", 424 expectedFrom, s.getLocalAddress().getHostAddress()); 425 } 426 427 try { 428 if (expectedFrom != null) { 429 s.send(p); 430 s.receive(p); 431 MoreAsserts.assertEquals(data, p.getData()); 432 } else { 433 try { 434 s.send(p); 435 s.receive(p); 436 fail("Received unexpected reply"); 437 } catch(IOException expected) {} 438 } 439 } finally { 440 s.close(); 441 } 442 } 443 checkTrafficOnVpn()444 private void checkTrafficOnVpn() throws IOException, ErrnoException { 445 checkUdpEcho("192.0.2.251", "192.0.2.2"); 446 checkUdpEcho("2001:db8:dead:beef::f00", "2001:db8:1:2::ffe"); 447 checkPing("2001:db8:dead:beef::f00"); 448 checkTcpReflection("192.0.2.252", "192.0.2.2"); 449 checkTcpReflection("2001:db8:dead:beef::f00", "2001:db8:1:2::ffe"); 450 } 451 checkNoTrafficOnVpn()452 private void checkNoTrafficOnVpn() throws IOException, ErrnoException { 453 checkUdpEcho("192.0.2.251", null); 454 checkUdpEcho("2001:db8:dead:beef::f00", null); 455 checkTcpReflection("192.0.2.252", null); 456 checkTcpReflection("2001:db8:dead:beef::f00", null); 457 } 458 testDefault()459 public void testDefault() throws Exception { 460 if (!supportedHardware()) return; 461 462 startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, 463 new String[] {"0.0.0.0/0", "::/0"}, 464 "", ""); 465 466 checkTrafficOnVpn(); 467 } 468 testAppAllowed()469 public void testAppAllowed() throws Exception { 470 if (!supportedHardware()) return; 471 472 startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, 473 new String[] {"192.0.2.0/24", "2001:db8::/32"}, 474 mPackageName, ""); 475 476 checkTrafficOnVpn(); 477 } 478 testAppDisallowed()479 public void testAppDisallowed() throws Exception { 480 if (!supportedHardware()) return; 481 482 startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, 483 new String[] {"192.0.2.0/24", "2001:db8::/32"}, 484 "", mPackageName); 485 486 checkNoTrafficOnVpn(); 487 } 488 } 489