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