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