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