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.os.ParcelFileDescriptor; 31 import android.os.Process; 32 import android.support.test.uiautomator.UiDevice; 33 import android.support.test.uiautomator.UiObject; 34 import android.support.test.uiautomator.UiObjectNotFoundException; 35 import android.support.test.uiautomator.UiScrollable; 36 import android.support.test.uiautomator.UiSelector; 37 import android.system.ErrnoException; 38 import android.system.Os; 39 import android.system.StructPollfd; 40 import android.test.InstrumentationTestCase; 41 import android.test.MoreAsserts; 42 import android.text.TextUtils; 43 import android.util.Log; 44 45 import com.android.cts.net.hostside.IRemoteSocketFactory; 46 47 import java.io.BufferedReader; 48 import java.io.Closeable; 49 import java.io.FileDescriptor; 50 import java.io.FileOutputStream; 51 import java.io.FileInputStream; 52 import java.io.InputStreamReader; 53 import java.io.IOException; 54 import java.io.InputStream; 55 import java.io.OutputStream; 56 import java.io.PrintWriter; 57 import java.net.DatagramPacket; 58 import java.net.DatagramSocket; 59 import java.net.Inet6Address; 60 import java.net.InetAddress; 61 import java.net.InetSocketAddress; 62 import java.net.ServerSocket; 63 import java.net.Socket; 64 import java.net.SocketException; 65 import java.nio.charset.StandardCharsets; 66 import java.util.Random; 67 68 /** 69 * Tests for the VpnService API. 70 * 71 * These tests establish a VPN via the VpnService API, and have the service reflect the packets back 72 * to the device without causing any network traffic. This allows testing the local VPN data path 73 * without a network connection or a VPN server. 74 * 75 * Note: in Lollipop, VPN functionality relies on kernel support for UID-based routing. If these 76 * tests fail, it may be due to the lack of kernel support. The necessary patches can be 77 * cherry-picked from the Android common kernel trees: 78 * 79 * android-3.10: 80 * https://android-review.googlesource.com/#/c/99220/ 81 * https://android-review.googlesource.com/#/c/100545/ 82 * 83 * android-3.4: 84 * https://android-review.googlesource.com/#/c/99225/ 85 * https://android-review.googlesource.com/#/c/100557/ 86 * 87 * To ensure that the kernel has the required commits, run the kernel unit 88 * tests described at: 89 * 90 * https://source.android.com/devices/tech/config/kernel_network_tests.html 91 * 92 */ 93 public class VpnTest extends InstrumentationTestCase { 94 95 public static String TAG = "VpnTest"; 96 public static int TIMEOUT_MS = 3 * 1000; 97 public static int SOCKET_TIMEOUT_MS = 100; 98 public static String TEST_HOST = "connectivitycheck.gstatic.com"; 99 100 private UiDevice mDevice; 101 private MyActivity mActivity; 102 private String mPackageName; 103 private ConnectivityManager mCM; 104 private RemoteSocketFactoryClient mRemoteSocketFactoryClient; 105 106 Network mNetwork; 107 NetworkCallback mCallback; 108 final Object mLock = new Object(); 109 final Object mLockShutdown = new Object(); 110 supportedHardware()111 private boolean supportedHardware() { 112 final PackageManager pm = getInstrumentation().getContext().getPackageManager(); 113 return !pm.hasSystemFeature("android.hardware.type.watch"); 114 } 115 116 @Override setUp()117 public void setUp() throws Exception { 118 super.setUp(); 119 120 mNetwork = null; 121 mCallback = null; 122 123 mDevice = UiDevice.getInstance(getInstrumentation()); 124 mActivity = launchActivity(getInstrumentation().getTargetContext().getPackageName(), 125 MyActivity.class, null); 126 mPackageName = mActivity.getPackageName(); 127 mCM = (ConnectivityManager) mActivity.getSystemService(mActivity.CONNECTIVITY_SERVICE); 128 mRemoteSocketFactoryClient = new RemoteSocketFactoryClient(mActivity); 129 mRemoteSocketFactoryClient.bind(); 130 mDevice.waitForIdle(); 131 } 132 133 @Override tearDown()134 public void tearDown() throws Exception { 135 mRemoteSocketFactoryClient.unbind(); 136 if (mCallback != null) { 137 mCM.unregisterNetworkCallback(mCallback); 138 } 139 Log.i(TAG, "Stopping VPN"); 140 stopVpn(); 141 mActivity.finish(); 142 super.tearDown(); 143 } 144 prepareVpn()145 private void prepareVpn() throws Exception { 146 final int REQUEST_ID = 42; 147 148 // Attempt to prepare. 149 Log.i(TAG, "Preparing VPN"); 150 Intent intent = VpnService.prepare(mActivity); 151 152 if (intent != null) { 153 // Start the confirmation dialog and click OK. 154 mActivity.startActivityForResult(intent, REQUEST_ID); 155 mDevice.waitForIdle(); 156 157 String packageName = intent.getComponent().getPackageName(); 158 String resourceIdRegex = "android:id/button1$|button_start_vpn"; 159 final UiObject okButton = new UiObject(new UiSelector() 160 .className("android.widget.Button") 161 .packageName(packageName) 162 .resourceIdMatches(resourceIdRegex)); 163 if (okButton.waitForExists(TIMEOUT_MS) == false) { 164 mActivity.finishActivity(REQUEST_ID); 165 fail("VpnService.prepare returned an Intent for '" + intent.getComponent() + "' " + 166 "to display the VPN confirmation dialog, but this test could not find the " + 167 "button to allow the VPN application to connect. Please ensure that the " + 168 "component displays a button with a resource ID matching the regexp: '" + 169 resourceIdRegex + "'."); 170 } 171 172 // Click the button and wait for RESULT_OK. 173 okButton.click(); 174 try { 175 int result = mActivity.getResult(TIMEOUT_MS); 176 if (result != MyActivity.RESULT_OK) { 177 fail("The VPN confirmation dialog did not return RESULT_OK when clicking on " + 178 "the button matching the regular expression '" + resourceIdRegex + 179 "' of " + intent.getComponent() + "'. Please ensure that clicking on " + 180 "that button allows the VPN application to connect. " + 181 "Return value: " + result); 182 } 183 } catch (InterruptedException e) { 184 fail("VPN confirmation dialog did not return after " + TIMEOUT_MS + "ms"); 185 } 186 187 // Now we should be prepared. 188 intent = VpnService.prepare(mActivity); 189 if (intent != null) { 190 fail("VpnService.prepare returned non-null even after the VPN dialog " + 191 intent.getComponent() + "returned RESULT_OK."); 192 } 193 } 194 } 195 startVpn( String[] addresses, String[] routes, String allowedApplications, String disallowedApplications)196 private void startVpn( 197 String[] addresses, String[] routes, 198 String allowedApplications, String disallowedApplications) throws Exception { 199 200 prepareVpn(); 201 202 // Register a callback so we will be notified when our VPN comes up. 203 final NetworkRequest request = new NetworkRequest.Builder() 204 .addTransportType(NetworkCapabilities.TRANSPORT_VPN) 205 .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) 206 .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) 207 .build(); 208 mCallback = new NetworkCallback() { 209 public void onAvailable(Network network) { 210 synchronized (mLock) { 211 Log.i(TAG, "Got available callback for network=" + network); 212 mNetwork = network; 213 mLock.notify(); 214 } 215 } 216 }; 217 mCM.registerNetworkCallback(request, mCallback); // Unregistered in tearDown. 218 219 // Start the service and wait up for TIMEOUT_MS ms for the VPN to come up. 220 Intent intent = new Intent(mActivity, MyVpnService.class) 221 .putExtra(mPackageName + ".cmd", "connect") 222 .putExtra(mPackageName + ".addresses", TextUtils.join(",", addresses)) 223 .putExtra(mPackageName + ".routes", TextUtils.join(",", routes)) 224 .putExtra(mPackageName + ".allowedapplications", allowedApplications) 225 .putExtra(mPackageName + ".disallowedapplications", disallowedApplications); 226 mActivity.startService(intent); 227 synchronized (mLock) { 228 if (mNetwork == null) { 229 Log.i(TAG, "bf mLock"); 230 mLock.wait(TIMEOUT_MS); 231 Log.i(TAG, "af mLock"); 232 } 233 } 234 235 if (mNetwork == null) { 236 fail("VPN did not become available after " + TIMEOUT_MS + "ms"); 237 } 238 239 // Unfortunately, when the available callback fires, the VPN UID ranges are not yet 240 // configured. Give the system some time to do so. http://b/18436087 . 241 try { Thread.sleep(3000); } catch(InterruptedException e) {} 242 } 243 stopVpn()244 private void stopVpn() { 245 // Register a callback so we will be notified when our VPN comes up. 246 final NetworkRequest request = new NetworkRequest.Builder() 247 .addTransportType(NetworkCapabilities.TRANSPORT_VPN) 248 .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) 249 .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) 250 .build(); 251 mCallback = new NetworkCallback() { 252 public void onLost(Network network) { 253 synchronized (mLockShutdown) { 254 Log.i(TAG, "Got lost callback for network=" + network + ",mNetwork = " + mNetwork); 255 if( mNetwork == network){ 256 mLockShutdown.notify(); 257 } 258 } 259 } 260 }; 261 mCM.registerNetworkCallback(request, mCallback); // Unregistered in tearDown. 262 // Simply calling mActivity.stopService() won't stop the service, because the system binds 263 // to the service for the purpose of sending it a revoke command if another VPN comes up, 264 // and stopping a bound service has no effect. Instead, "start" the service again with an 265 // Intent that tells it to disconnect. 266 Intent intent = new Intent(mActivity, MyVpnService.class) 267 .putExtra(mPackageName + ".cmd", "disconnect"); 268 mActivity.startService(intent); 269 synchronized (mLockShutdown) { 270 try { 271 Log.i(TAG, "bf mLockShutdown"); 272 mLockShutdown.wait(TIMEOUT_MS); 273 Log.i(TAG, "af mLockShutdown"); 274 } catch(InterruptedException e) {} 275 } 276 } 277 closeQuietly(Closeable c)278 private static void closeQuietly(Closeable c) { 279 if (c != null) { 280 try { 281 c.close(); 282 } catch (IOException e) { 283 } 284 } 285 } 286 checkPing(String to)287 private static void checkPing(String to) throws IOException, ErrnoException { 288 InetAddress address = InetAddress.getByName(to); 289 FileDescriptor s; 290 final int LENGTH = 64; 291 byte[] packet = new byte[LENGTH]; 292 byte[] header; 293 294 // Construct a ping packet. 295 Random random = new Random(); 296 random.nextBytes(packet); 297 if (address instanceof Inet6Address) { 298 s = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6); 299 header = new byte[] { (byte) 0x80, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; 300 } else { 301 // Note that this doesn't actually work due to http://b/18558481 . 302 s = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP); 303 header = new byte[] { (byte) 0x08, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; 304 } 305 System.arraycopy(header, 0, packet, 0, header.length); 306 307 // Send the packet. 308 int port = random.nextInt(65534) + 1; 309 Os.connect(s, address, port); 310 Os.write(s, packet, 0, packet.length); 311 312 // Expect a reply. 313 StructPollfd pollfd = new StructPollfd(); 314 pollfd.events = (short) POLLIN; // "error: possible loss of precision" 315 pollfd.fd = s; 316 int ret = Os.poll(new StructPollfd[] { pollfd }, SOCKET_TIMEOUT_MS); 317 assertEquals("Expected reply after sending ping", 1, ret); 318 319 byte[] reply = new byte[LENGTH]; 320 int read = Os.read(s, reply, 0, LENGTH); 321 assertEquals(LENGTH, read); 322 323 // Find out what the kernel set the ICMP ID to. 324 InetSocketAddress local = (InetSocketAddress) Os.getsockname(s); 325 port = local.getPort(); 326 packet[4] = (byte) ((port >> 8) & 0xff); 327 packet[5] = (byte) (port & 0xff); 328 329 // Check the contents. 330 if (packet[0] == (byte) 0x80) { 331 packet[0] = (byte) 0x81; 332 } else { 333 packet[0] = 0; 334 } 335 // Zero out the checksum in the reply so it matches the uninitialized checksum in packet. 336 reply[2] = reply[3] = 0; 337 MoreAsserts.assertEquals(packet, reply); 338 } 339 340 // Writes data to out and checks that it appears identically on in. writeAndCheckData( OutputStream out, InputStream in, byte[] data)341 private static void writeAndCheckData( 342 OutputStream out, InputStream in, byte[] data) throws IOException { 343 out.write(data, 0, data.length); 344 out.flush(); 345 346 byte[] read = new byte[data.length]; 347 int bytesRead = 0, totalRead = 0; 348 do { 349 bytesRead = in.read(read, totalRead, read.length - totalRead); 350 totalRead += bytesRead; 351 } while (bytesRead >= 0 && totalRead < data.length); 352 assertEquals(totalRead, data.length); 353 MoreAsserts.assertEquals(data, read); 354 } 355 checkTcpReflection(String to, String expectedFrom)356 private static void checkTcpReflection(String to, String expectedFrom) throws IOException { 357 // Exercise TCP over the VPN by "connecting to ourselves". We open a server socket and a 358 // client socket, and connect the client socket to a remote host, with the port of the 359 // server socket. The PacketReflector reflects the packets, changing the source addresses 360 // but not the ports, so our client socket is connected to our server socket, though both 361 // sockets think their peers are on the "remote" IP address. 362 363 // Open a listening socket. 364 ServerSocket listen = new ServerSocket(0, 10, InetAddress.getByName("::")); 365 366 // Connect the client socket to it. 367 InetAddress toAddr = InetAddress.getByName(to); 368 Socket client = new Socket(); 369 try { 370 client.connect(new InetSocketAddress(toAddr, listen.getLocalPort()), SOCKET_TIMEOUT_MS); 371 if (expectedFrom == null) { 372 closeQuietly(listen); 373 closeQuietly(client); 374 fail("Expected connection to fail, but it succeeded."); 375 } 376 } catch (IOException e) { 377 if (expectedFrom != null) { 378 closeQuietly(listen); 379 fail("Expected connection to succeed, but it failed."); 380 } else { 381 // We expected the connection to fail, and it did, so there's nothing more to test. 382 return; 383 } 384 } 385 386 // The connection succeeded, and we expected it to succeed. Send some data; if things are 387 // working, the data will be sent to the VPN, reflected by the PacketReflector, and arrive 388 // at our server socket. For good measure, send some data in the other direction. 389 Socket server = null; 390 try { 391 // Accept the connection on the server side. 392 listen.setSoTimeout(SOCKET_TIMEOUT_MS); 393 server = listen.accept(); 394 395 // Check that the source and peer addresses are as expected. 396 assertEquals(expectedFrom, client.getLocalAddress().getHostAddress()); 397 assertEquals(expectedFrom, server.getLocalAddress().getHostAddress()); 398 assertEquals( 399 new InetSocketAddress(toAddr, client.getLocalPort()), 400 server.getRemoteSocketAddress()); 401 assertEquals( 402 new InetSocketAddress(toAddr, server.getLocalPort()), 403 client.getRemoteSocketAddress()); 404 405 // Now write some data. 406 final int LENGTH = 32768; 407 byte[] data = new byte[LENGTH]; 408 new Random().nextBytes(data); 409 410 // Make sure our writes don't block or time out, because we're single-threaded and can't 411 // read and write at the same time. 412 server.setReceiveBufferSize(LENGTH * 2); 413 client.setSendBufferSize(LENGTH * 2); 414 client.setSoTimeout(SOCKET_TIMEOUT_MS); 415 server.setSoTimeout(SOCKET_TIMEOUT_MS); 416 417 // Send some data from client to server, then from server to client. 418 writeAndCheckData(client.getOutputStream(), server.getInputStream(), data); 419 writeAndCheckData(server.getOutputStream(), client.getInputStream(), data); 420 } finally { 421 closeQuietly(listen); 422 closeQuietly(client); 423 closeQuietly(server); 424 } 425 } 426 checkUdpEcho(String to, String expectedFrom)427 private static void checkUdpEcho(String to, String expectedFrom) throws IOException { 428 DatagramSocket s; 429 InetAddress address = InetAddress.getByName(to); 430 if (address instanceof Inet6Address) { // http://b/18094870 431 s = new DatagramSocket(0, InetAddress.getByName("::")); 432 } else { 433 s = new DatagramSocket(); 434 } 435 s.setSoTimeout(SOCKET_TIMEOUT_MS); 436 437 Random random = new Random(); 438 byte[] data = new byte[random.nextInt(1650)]; 439 random.nextBytes(data); 440 DatagramPacket p = new DatagramPacket(data, data.length); 441 s.connect(address, 7); 442 443 if (expectedFrom != null) { 444 assertEquals("Unexpected source address: ", 445 expectedFrom, s.getLocalAddress().getHostAddress()); 446 } 447 448 try { 449 if (expectedFrom != null) { 450 s.send(p); 451 s.receive(p); 452 MoreAsserts.assertEquals(data, p.getData()); 453 } else { 454 try { 455 s.send(p); 456 s.receive(p); 457 fail("Received unexpected reply"); 458 } catch(IOException expected) {} 459 } 460 } finally { 461 s.close(); 462 } 463 } 464 checkTrafficOnVpn()465 private void checkTrafficOnVpn() throws Exception { 466 checkUdpEcho("192.0.2.251", "192.0.2.2"); 467 checkUdpEcho("2001:db8:dead:beef::f00", "2001:db8:1:2::ffe"); 468 checkPing("2001:db8:dead:beef::f00"); 469 checkTcpReflection("192.0.2.252", "192.0.2.2"); 470 checkTcpReflection("2001:db8:dead:beef::f00", "2001:db8:1:2::ffe"); 471 } 472 checkNoTrafficOnVpn()473 private void checkNoTrafficOnVpn() throws Exception { 474 checkUdpEcho("192.0.2.251", null); 475 checkUdpEcho("2001:db8:dead:beef::f00", null); 476 checkTcpReflection("192.0.2.252", null); 477 checkTcpReflection("2001:db8:dead:beef::f00", null); 478 } 479 openSocketFd(String host, int port, int timeoutMs)480 private FileDescriptor openSocketFd(String host, int port, int timeoutMs) throws Exception { 481 Socket s = new Socket(host, port); 482 s.setSoTimeout(timeoutMs); 483 // Dup the filedescriptor so ParcelFileDescriptor's finalizer doesn't garbage collect it 484 // and cause our fd to become invalid. http://b/35927643 . 485 FileDescriptor fd = Os.dup(ParcelFileDescriptor.fromSocket(s).getFileDescriptor()); 486 s.close(); 487 return fd; 488 } 489 openSocketFdInOtherApp( String host, int port, int timeoutMs)490 private FileDescriptor openSocketFdInOtherApp( 491 String host, int port, int timeoutMs) throws Exception { 492 Log.d(TAG, String.format("Creating test socket in UID=%d, my UID=%d", 493 mRemoteSocketFactoryClient.getUid(), Os.getuid())); 494 FileDescriptor fd = mRemoteSocketFactoryClient.openSocketFd(host, port, TIMEOUT_MS); 495 return fd; 496 } 497 sendRequest(FileDescriptor fd, String host)498 private void sendRequest(FileDescriptor fd, String host) throws Exception { 499 String request = "GET /generate_204 HTTP/1.1\r\n" + 500 "Host: " + host + "\r\n" + 501 "Connection: keep-alive\r\n\r\n"; 502 byte[] requestBytes = request.getBytes(StandardCharsets.UTF_8); 503 int ret = Os.write(fd, requestBytes, 0, requestBytes.length); 504 Log.d(TAG, "Wrote " + ret + "bytes"); 505 506 String expected = "HTTP/1.1 204 No Content\r\n"; 507 byte[] response = new byte[expected.length()]; 508 Os.read(fd, response, 0, response.length); 509 510 String actual = new String(response, StandardCharsets.UTF_8); 511 assertEquals(expected, actual); 512 Log.d(TAG, "Got response: " + actual); 513 } 514 assertSocketStillOpen(FileDescriptor fd, String host)515 private void assertSocketStillOpen(FileDescriptor fd, String host) throws Exception { 516 try { 517 assertTrue(fd.valid()); 518 sendRequest(fd, host); 519 assertTrue(fd.valid()); 520 } finally { 521 Os.close(fd); 522 } 523 } 524 assertSocketClosed(FileDescriptor fd, String host)525 private void assertSocketClosed(FileDescriptor fd, String host) throws Exception { 526 try { 527 assertTrue(fd.valid()); 528 sendRequest(fd, host); 529 fail("Socket opened before VPN connects should be closed when VPN connects"); 530 } catch (ErrnoException expected) { 531 assertEquals(ECONNABORTED, expected.errno); 532 assertTrue(fd.valid()); 533 } finally { 534 Os.close(fd); 535 } 536 } 537 testDefault()538 public void testDefault() throws Exception { 539 if (!supportedHardware()) return; 540 541 FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS); 542 543 startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, 544 new String[] {"0.0.0.0/0", "::/0"}, 545 "", ""); 546 547 assertSocketClosed(fd, TEST_HOST); 548 549 checkTrafficOnVpn(); 550 } 551 testAppAllowed()552 public void testAppAllowed() throws Exception { 553 if (!supportedHardware()) return; 554 555 FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS); 556 557 String allowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName; 558 startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, 559 new String[] {"192.0.2.0/24", "2001:db8::/32"}, 560 allowedApps, ""); 561 562 assertSocketClosed(fd, TEST_HOST); 563 564 checkTrafficOnVpn(); 565 } 566 testAppDisallowed()567 public void testAppDisallowed() throws Exception { 568 if (!supportedHardware()) return; 569 570 FileDescriptor localFd = openSocketFd(TEST_HOST, 80, TIMEOUT_MS); 571 FileDescriptor remoteFd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS); 572 573 String disallowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName; 574 startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, 575 new String[] {"192.0.2.0/24", "2001:db8::/32"}, 576 "", disallowedApps); 577 578 assertSocketStillOpen(localFd, TEST_HOST); 579 assertSocketStillOpen(remoteFd, TEST_HOST); 580 581 checkNoTrafficOnVpn(); 582 } 583 } 584