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.os.Process.INVALID_UID;
20 import static android.system.OsConstants.*;
21 
22 import android.annotation.Nullable;
23 import android.content.ContentResolver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.pm.PackageManager;
27 import android.net.ConnectivityManager;
28 import android.net.ConnectivityManager.NetworkCallback;
29 import android.net.LinkProperties;
30 import android.net.Network;
31 import android.net.NetworkCapabilities;
32 import android.net.NetworkRequest;
33 import android.net.Proxy;
34 import android.net.ProxyInfo;
35 import android.net.VpnService;
36 import android.net.wifi.WifiManager;
37 import android.provider.Settings;
38 import android.os.ParcelFileDescriptor;
39 import android.os.Process;
40 import android.os.SystemProperties;
41 import android.support.test.uiautomator.UiDevice;
42 import android.support.test.uiautomator.UiObject;
43 import android.support.test.uiautomator.UiSelector;
44 import android.system.ErrnoException;
45 import android.system.Os;
46 import android.system.OsConstants;
47 import android.system.StructPollfd;
48 import android.test.InstrumentationTestCase;
49 import android.test.MoreAsserts;
50 import android.text.TextUtils;
51 import android.util.Log;
52 
53 import com.android.compatibility.common.util.BlockingBroadcastReceiver;
54 
55 import java.io.Closeable;
56 import java.io.FileDescriptor;
57 import java.io.IOException;
58 import java.io.InputStream;
59 import java.io.OutputStream;
60 import java.net.DatagramPacket;
61 import java.net.DatagramSocket;
62 import java.net.Inet6Address;
63 import java.net.InetAddress;
64 import java.net.InetSocketAddress;
65 import java.net.ServerSocket;
66 import java.net.Socket;
67 import java.net.SocketException;
68 import java.net.UnknownHostException;
69 import java.nio.charset.StandardCharsets;
70 import java.util.ArrayList;
71 import java.util.Objects;
72 import java.util.Random;
73 import java.util.concurrent.CountDownLatch;
74 import java.util.concurrent.TimeUnit;
75 
76 /**
77  * Tests for the VpnService API.
78  *
79  * These tests establish a VPN via the VpnService API, and have the service reflect the packets back
80  * to the device without causing any network traffic. This allows testing the local VPN data path
81  * without a network connection or a VPN server.
82  *
83  * Note: in Lollipop, VPN functionality relies on kernel support for UID-based routing. If these
84  * tests fail, it may be due to the lack of kernel support. The necessary patches can be
85  * cherry-picked from the Android common kernel trees:
86  *
87  * android-3.10:
88  *   https://android-review.googlesource.com/#/c/99220/
89  *   https://android-review.googlesource.com/#/c/100545/
90  *
91  * android-3.4:
92  *   https://android-review.googlesource.com/#/c/99225/
93  *   https://android-review.googlesource.com/#/c/100557/
94  *
95  * To ensure that the kernel has the required commits, run the kernel unit
96  * tests described at:
97  *
98  *   https://source.android.com/devices/tech/config/kernel_network_tests.html
99  *
100  */
101 public class VpnTest extends InstrumentationTestCase {
102 
103     // These are neither public nor @TestApi.
104     // TODO: add them to @TestApi.
105     private static final String PRIVATE_DNS_MODE_SETTING = "private_dns_mode";
106     private static final String PRIVATE_DNS_MODE_PROVIDER_HOSTNAME = "hostname";
107     private static final String PRIVATE_DNS_MODE_OPPORTUNISTIC = "opportunistic";
108     private static final String PRIVATE_DNS_SPECIFIER_SETTING = "private_dns_specifier";
109 
110     public static String TAG = "VpnTest";
111     public static int TIMEOUT_MS = 3 * 1000;
112     public static int SOCKET_TIMEOUT_MS = 100;
113     public static String TEST_HOST = "connectivitycheck.gstatic.com";
114 
115     private UiDevice mDevice;
116     private MyActivity mActivity;
117     private String mPackageName;
118     private ConnectivityManager mCM;
119     private WifiManager mWifiManager;
120     private RemoteSocketFactoryClient mRemoteSocketFactoryClient;
121 
122     Network mNetwork;
123     NetworkCallback mCallback;
124     final Object mLock = new Object();
125     final Object mLockShutdown = new Object();
126 
127     private String mOldPrivateDnsMode;
128     private String mOldPrivateDnsSpecifier;
129 
supportedHardware()130     private boolean supportedHardware() {
131         final PackageManager pm = getInstrumentation().getContext().getPackageManager();
132         return !pm.hasSystemFeature("android.hardware.type.watch");
133     }
134 
135     @Override
setUp()136     public void setUp() throws Exception {
137         super.setUp();
138 
139         mNetwork = null;
140         mCallback = null;
141         storePrivateDnsSetting();
142 
143         mDevice = UiDevice.getInstance(getInstrumentation());
144         mActivity = launchActivity(getInstrumentation().getTargetContext().getPackageName(),
145                 MyActivity.class, null);
146         mPackageName = mActivity.getPackageName();
147         mCM = (ConnectivityManager) mActivity.getSystemService(Context.CONNECTIVITY_SERVICE);
148         mWifiManager = (WifiManager) mActivity.getSystemService(Context.WIFI_SERVICE);
149         mRemoteSocketFactoryClient = new RemoteSocketFactoryClient(mActivity);
150         mRemoteSocketFactoryClient.bind();
151         mDevice.waitForIdle();
152     }
153 
154     @Override
tearDown()155     public void tearDown() throws Exception {
156         restorePrivateDnsSetting();
157         mRemoteSocketFactoryClient.unbind();
158         if (mCallback != null) {
159             mCM.unregisterNetworkCallback(mCallback);
160         }
161         Log.i(TAG, "Stopping VPN");
162         stopVpn();
163         mActivity.finish();
164         super.tearDown();
165     }
166 
prepareVpn()167     private void prepareVpn() throws Exception {
168         final int REQUEST_ID = 42;
169 
170         // Attempt to prepare.
171         Log.i(TAG, "Preparing VPN");
172         Intent intent = VpnService.prepare(mActivity);
173 
174         if (intent != null) {
175             // Start the confirmation dialog and click OK.
176             mActivity.startActivityForResult(intent, REQUEST_ID);
177             mDevice.waitForIdle();
178 
179             String packageName = intent.getComponent().getPackageName();
180             String resourceIdRegex = "android:id/button1$|button_start_vpn";
181             final UiObject okButton = new UiObject(new UiSelector()
182                     .className("android.widget.Button")
183                     .packageName(packageName)
184                     .resourceIdMatches(resourceIdRegex));
185             if (okButton.waitForExists(TIMEOUT_MS) == false) {
186                 mActivity.finishActivity(REQUEST_ID);
187                 fail("VpnService.prepare returned an Intent for '" + intent.getComponent() + "' " +
188                      "to display the VPN confirmation dialog, but this test could not find the " +
189                      "button to allow the VPN application to connect. Please ensure that the "  +
190                      "component displays a button with a resource ID matching the regexp: '" +
191                      resourceIdRegex + "'.");
192             }
193 
194             // Click the button and wait for RESULT_OK.
195             okButton.click();
196             try {
197                 int result = mActivity.getResult(TIMEOUT_MS);
198                 if (result != MyActivity.RESULT_OK) {
199                     fail("The VPN confirmation dialog did not return RESULT_OK when clicking on " +
200                          "the button matching the regular expression '" + resourceIdRegex +
201                          "' of " + intent.getComponent() + "'. Please ensure that clicking on " +
202                          "that button allows the VPN application to connect. " +
203                          "Return value: " + result);
204                 }
205             } catch (InterruptedException e) {
206                 fail("VPN confirmation dialog did not return after " + TIMEOUT_MS + "ms");
207             }
208 
209             // Now we should be prepared.
210             intent = VpnService.prepare(mActivity);
211             if (intent != null) {
212                 fail("VpnService.prepare returned non-null even after the VPN dialog " +
213                      intent.getComponent() + "returned RESULT_OK.");
214             }
215         }
216     }
217 
218     // TODO: Consider replacing arguments with a Builder.
startVpn( String[] addresses, String[] routes, String allowedApplications, String disallowedApplications, @Nullable ProxyInfo proxyInfo, @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered)219     private void startVpn(
220         String[] addresses, String[] routes, String allowedApplications,
221         String disallowedApplications, @Nullable ProxyInfo proxyInfo,
222         @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered) throws Exception {
223         prepareVpn();
224 
225         // Register a callback so we will be notified when our VPN comes up.
226         final NetworkRequest request = new NetworkRequest.Builder()
227                 .addTransportType(NetworkCapabilities.TRANSPORT_VPN)
228                 .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
229                 .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
230                 .build();
231         mCallback = new NetworkCallback() {
232             public void onAvailable(Network network) {
233                 synchronized (mLock) {
234                     Log.i(TAG, "Got available callback for network=" + network);
235                     mNetwork = network;
236                     mLock.notify();
237                 }
238             }
239         };
240         mCM.registerNetworkCallback(request, mCallback);  // Unregistered in tearDown.
241 
242         // Start the service and wait up for TIMEOUT_MS ms for the VPN to come up.
243         Intent intent = new Intent(mActivity, MyVpnService.class)
244                 .putExtra(mPackageName + ".cmd", "connect")
245                 .putExtra(mPackageName + ".addresses", TextUtils.join(",", addresses))
246                 .putExtra(mPackageName + ".routes", TextUtils.join(",", routes))
247                 .putExtra(mPackageName + ".allowedapplications", allowedApplications)
248                 .putExtra(mPackageName + ".disallowedapplications", disallowedApplications)
249                 .putExtra(mPackageName + ".httpProxy", proxyInfo)
250                 .putParcelableArrayListExtra(
251                     mPackageName + ".underlyingNetworks", underlyingNetworks)
252                 .putExtra(mPackageName + ".isAlwaysMetered", isAlwaysMetered);
253 
254         mActivity.startService(intent);
255         synchronized (mLock) {
256             if (mNetwork == null) {
257                  Log.i(TAG, "bf mLock");
258                  mLock.wait(TIMEOUT_MS);
259                  Log.i(TAG, "af mLock");
260             }
261         }
262 
263         if (mNetwork == null) {
264             fail("VPN did not become available after " + TIMEOUT_MS + "ms");
265         }
266 
267         // Unfortunately, when the available callback fires, the VPN UID ranges are not yet
268         // configured. Give the system some time to do so. http://b/18436087 .
269         try { Thread.sleep(3000); } catch(InterruptedException e) {}
270     }
271 
stopVpn()272     private void stopVpn() {
273         // Register a callback so we will be notified when our VPN comes up.
274         final NetworkRequest request = new NetworkRequest.Builder()
275                 .addTransportType(NetworkCapabilities.TRANSPORT_VPN)
276                 .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
277                 .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
278                 .build();
279         mCallback = new NetworkCallback() {
280             public void onLost(Network network) {
281                 synchronized (mLockShutdown) {
282                     Log.i(TAG, "Got lost callback for network=" + network
283                             + ",mNetwork = " + mNetwork);
284                     if( mNetwork == network){
285                         mLockShutdown.notify();
286                     }
287                 }
288             }
289        };
290         mCM.registerNetworkCallback(request, mCallback);  // Unregistered in tearDown.
291         // Simply calling mActivity.stopService() won't stop the service, because the system binds
292         // to the service for the purpose of sending it a revoke command if another VPN comes up,
293         // and stopping a bound service has no effect. Instead, "start" the service again with an
294         // Intent that tells it to disconnect.
295         Intent intent = new Intent(mActivity, MyVpnService.class)
296                 .putExtra(mPackageName + ".cmd", "disconnect");
297         mActivity.startService(intent);
298         synchronized (mLockShutdown) {
299             try {
300                  Log.i(TAG, "bf mLockShutdown");
301                  mLockShutdown.wait(TIMEOUT_MS);
302                  Log.i(TAG, "af mLockShutdown");
303             } catch(InterruptedException e) {}
304         }
305     }
306 
closeQuietly(Closeable c)307     private static void closeQuietly(Closeable c) {
308         if (c != null) {
309             try {
310                 c.close();
311             } catch (IOException e) {
312             }
313         }
314     }
315 
checkPing(String to)316     private static void checkPing(String to) throws IOException, ErrnoException {
317         InetAddress address = InetAddress.getByName(to);
318         FileDescriptor s;
319         final int LENGTH = 64;
320         byte[] packet = new byte[LENGTH];
321         byte[] header;
322 
323         // Construct a ping packet.
324         Random random = new Random();
325         random.nextBytes(packet);
326         if (address instanceof Inet6Address) {
327             s = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6);
328             header = new byte[] { (byte) 0x80, (byte) 0x00, (byte) 0x00, (byte) 0x00 };
329         } else {
330             // Note that this doesn't actually work due to http://b/18558481 .
331             s = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
332             header = new byte[] { (byte) 0x08, (byte) 0x00, (byte) 0x00, (byte) 0x00 };
333         }
334         System.arraycopy(header, 0, packet, 0, header.length);
335 
336         // Send the packet.
337         int port = random.nextInt(65534) + 1;
338         Os.connect(s, address, port);
339         Os.write(s, packet, 0, packet.length);
340 
341         // Expect a reply.
342         StructPollfd pollfd = new StructPollfd();
343         pollfd.events = (short) POLLIN;  // "error: possible loss of precision"
344         pollfd.fd = s;
345         int ret = Os.poll(new StructPollfd[] { pollfd }, SOCKET_TIMEOUT_MS);
346         assertEquals("Expected reply after sending ping", 1, ret);
347 
348         byte[] reply = new byte[LENGTH];
349         int read = Os.read(s, reply, 0, LENGTH);
350         assertEquals(LENGTH, read);
351 
352         // Find out what the kernel set the ICMP ID to.
353         InetSocketAddress local = (InetSocketAddress) Os.getsockname(s);
354         port = local.getPort();
355         packet[4] = (byte) ((port >> 8) & 0xff);
356         packet[5] = (byte) (port & 0xff);
357 
358         // Check the contents.
359         if (packet[0] == (byte) 0x80) {
360             packet[0] = (byte) 0x81;
361         } else {
362             packet[0] = 0;
363         }
364         // Zero out the checksum in the reply so it matches the uninitialized checksum in packet.
365         reply[2] = reply[3] = 0;
366         MoreAsserts.assertEquals(packet, reply);
367     }
368 
369     // Writes data to out and checks that it appears identically on in.
writeAndCheckData( OutputStream out, InputStream in, byte[] data)370     private static void writeAndCheckData(
371             OutputStream out, InputStream in, byte[] data) throws IOException {
372         out.write(data, 0, data.length);
373         out.flush();
374 
375         byte[] read = new byte[data.length];
376         int bytesRead = 0, totalRead = 0;
377         do {
378             bytesRead = in.read(read, totalRead, read.length - totalRead);
379             totalRead += bytesRead;
380         } while (bytesRead >= 0 && totalRead < data.length);
381         assertEquals(totalRead, data.length);
382         MoreAsserts.assertEquals(data, read);
383     }
384 
checkTcpReflection(String to, String expectedFrom)385     private void checkTcpReflection(String to, String expectedFrom) throws IOException {
386         // Exercise TCP over the VPN by "connecting to ourselves". We open a server socket and a
387         // client socket, and connect the client socket to a remote host, with the port of the
388         // server socket. The PacketReflector reflects the packets, changing the source addresses
389         // but not the ports, so our client socket is connected to our server socket, though both
390         // sockets think their peers are on the "remote" IP address.
391 
392         // Open a listening socket.
393         ServerSocket listen = new ServerSocket(0, 10, InetAddress.getByName("::"));
394 
395         // Connect the client socket to it.
396         InetAddress toAddr = InetAddress.getByName(to);
397         Socket client = new Socket();
398         try {
399             client.connect(new InetSocketAddress(toAddr, listen.getLocalPort()), SOCKET_TIMEOUT_MS);
400             if (expectedFrom == null) {
401                 closeQuietly(listen);
402                 closeQuietly(client);
403                 fail("Expected connection to fail, but it succeeded.");
404             }
405         } catch (IOException e) {
406             if (expectedFrom != null) {
407                 closeQuietly(listen);
408                 fail("Expected connection to succeed, but it failed.");
409             } else {
410                 // We expected the connection to fail, and it did, so there's nothing more to test.
411                 return;
412             }
413         }
414 
415         // The connection succeeded, and we expected it to succeed. Send some data; if things are
416         // working, the data will be sent to the VPN, reflected by the PacketReflector, and arrive
417         // at our server socket. For good measure, send some data in the other direction.
418         Socket server = null;
419         try {
420             // Accept the connection on the server side.
421             listen.setSoTimeout(SOCKET_TIMEOUT_MS);
422             server = listen.accept();
423             checkConnectionOwnerUidTcp(client);
424             checkConnectionOwnerUidTcp(server);
425             // Check that the source and peer addresses are as expected.
426             assertEquals(expectedFrom, client.getLocalAddress().getHostAddress());
427             assertEquals(expectedFrom, server.getLocalAddress().getHostAddress());
428             assertEquals(
429                     new InetSocketAddress(toAddr, client.getLocalPort()),
430                     server.getRemoteSocketAddress());
431             assertEquals(
432                     new InetSocketAddress(toAddr, server.getLocalPort()),
433                     client.getRemoteSocketAddress());
434 
435             // Now write some data.
436             final int LENGTH = 32768;
437             byte[] data = new byte[LENGTH];
438             new Random().nextBytes(data);
439 
440             // Make sure our writes don't block or time out, because we're single-threaded and can't
441             // read and write at the same time.
442             server.setReceiveBufferSize(LENGTH * 2);
443             client.setSendBufferSize(LENGTH * 2);
444             client.setSoTimeout(SOCKET_TIMEOUT_MS);
445             server.setSoTimeout(SOCKET_TIMEOUT_MS);
446 
447             // Send some data from client to server, then from server to client.
448             writeAndCheckData(client.getOutputStream(), server.getInputStream(), data);
449             writeAndCheckData(server.getOutputStream(), client.getInputStream(), data);
450         } finally {
451             closeQuietly(listen);
452             closeQuietly(client);
453             closeQuietly(server);
454         }
455     }
456 
checkConnectionOwnerUidUdp(DatagramSocket s, boolean expectSuccess)457     private void checkConnectionOwnerUidUdp(DatagramSocket s, boolean expectSuccess) {
458         final int expectedUid = expectSuccess ? Process.myUid() : INVALID_UID;
459         InetSocketAddress loc = new InetSocketAddress(s.getLocalAddress(), s.getLocalPort());
460         InetSocketAddress rem = new InetSocketAddress(s.getInetAddress(), s.getPort());
461         int uid = mCM.getConnectionOwnerUid(OsConstants.IPPROTO_UDP, loc, rem);
462         assertEquals(expectedUid, uid);
463     }
464 
checkConnectionOwnerUidTcp(Socket s)465     private void checkConnectionOwnerUidTcp(Socket s) {
466         final int expectedUid = Process.myUid();
467         InetSocketAddress loc = new InetSocketAddress(s.getLocalAddress(), s.getLocalPort());
468         InetSocketAddress rem = new InetSocketAddress(s.getInetAddress(), s.getPort());
469         int uid = mCM.getConnectionOwnerUid(OsConstants.IPPROTO_TCP, loc, rem);
470         assertEquals(expectedUid, uid);
471     }
472 
checkUdpEcho(String to, String expectedFrom)473     private void checkUdpEcho(String to, String expectedFrom) throws IOException {
474         DatagramSocket s;
475         InetAddress address = InetAddress.getByName(to);
476         if (address instanceof Inet6Address) {  // http://b/18094870
477             s = new DatagramSocket(0, InetAddress.getByName("::"));
478         } else {
479             s = new DatagramSocket();
480         }
481         s.setSoTimeout(SOCKET_TIMEOUT_MS);
482 
483         Random random = new Random();
484         byte[] data = new byte[random.nextInt(1650)];
485         random.nextBytes(data);
486         DatagramPacket p = new DatagramPacket(data, data.length);
487         s.connect(address, 7);
488 
489         if (expectedFrom != null) {
490             assertEquals("Unexpected source address: ",
491                          expectedFrom, s.getLocalAddress().getHostAddress());
492         }
493 
494         try {
495             if (expectedFrom != null) {
496                 s.send(p);
497                 checkConnectionOwnerUidUdp(s, true);
498                 s.receive(p);
499                 MoreAsserts.assertEquals(data, p.getData());
500             } else {
501                 try {
502                     s.send(p);
503                     s.receive(p);
504                     fail("Received unexpected reply");
505                 } catch (IOException expected) {
506                     checkConnectionOwnerUidUdp(s, false);
507                 }
508             }
509         } finally {
510             s.close();
511         }
512     }
513 
checkTrafficOnVpn()514     private void checkTrafficOnVpn() throws Exception {
515         checkUdpEcho("192.0.2.251", "192.0.2.2");
516         checkUdpEcho("2001:db8:dead:beef::f00", "2001:db8:1:2::ffe");
517         checkPing("2001:db8:dead:beef::f00");
518         checkTcpReflection("192.0.2.252", "192.0.2.2");
519         checkTcpReflection("2001:db8:dead:beef::f00", "2001:db8:1:2::ffe");
520     }
521 
checkNoTrafficOnVpn()522     private void checkNoTrafficOnVpn() throws Exception {
523         checkUdpEcho("192.0.2.251", null);
524         checkUdpEcho("2001:db8:dead:beef::f00", null);
525         checkTcpReflection("192.0.2.252", null);
526         checkTcpReflection("2001:db8:dead:beef::f00", null);
527     }
528 
openSocketFd(String host, int port, int timeoutMs)529     private FileDescriptor openSocketFd(String host, int port, int timeoutMs) throws Exception {
530         Socket s = new Socket(host, port);
531         s.setSoTimeout(timeoutMs);
532         // Dup the filedescriptor so ParcelFileDescriptor's finalizer doesn't garbage collect it
533         // and cause our fd to become invalid. http://b/35927643 .
534         FileDescriptor fd = Os.dup(ParcelFileDescriptor.fromSocket(s).getFileDescriptor());
535         s.close();
536         return fd;
537     }
538 
openSocketFdInOtherApp( String host, int port, int timeoutMs)539     private FileDescriptor openSocketFdInOtherApp(
540             String host, int port, int timeoutMs) throws Exception {
541         Log.d(TAG, String.format("Creating test socket in UID=%d, my UID=%d",
542                 mRemoteSocketFactoryClient.getUid(), Os.getuid()));
543         FileDescriptor fd = mRemoteSocketFactoryClient.openSocketFd(host, port, TIMEOUT_MS);
544         return fd;
545     }
546 
sendRequest(FileDescriptor fd, String host)547     private void sendRequest(FileDescriptor fd, String host) throws Exception {
548         String request = "GET /generate_204 HTTP/1.1\r\n" +
549                 "Host: " + host + "\r\n" +
550                 "Connection: keep-alive\r\n\r\n";
551         byte[] requestBytes = request.getBytes(StandardCharsets.UTF_8);
552         int ret = Os.write(fd, requestBytes, 0, requestBytes.length);
553         Log.d(TAG, "Wrote " + ret + "bytes");
554 
555         String expected = "HTTP/1.1 204 No Content\r\n";
556         byte[] response = new byte[expected.length()];
557         Os.read(fd, response, 0, response.length);
558 
559         String actual = new String(response, StandardCharsets.UTF_8);
560         assertEquals(expected, actual);
561         Log.d(TAG, "Got response: " + actual);
562     }
563 
assertSocketStillOpen(FileDescriptor fd, String host)564     private void assertSocketStillOpen(FileDescriptor fd, String host) throws Exception {
565         try {
566             assertTrue(fd.valid());
567             sendRequest(fd, host);
568             assertTrue(fd.valid());
569         } finally {
570             Os.close(fd);
571         }
572     }
573 
assertSocketClosed(FileDescriptor fd, String host)574     private void assertSocketClosed(FileDescriptor fd, String host) throws Exception {
575         try {
576             assertTrue(fd.valid());
577             sendRequest(fd, host);
578             fail("Socket opened before VPN connects should be closed when VPN connects");
579         } catch (ErrnoException expected) {
580             assertEquals(ECONNABORTED, expected.errno);
581             assertTrue(fd.valid());
582         } finally {
583             Os.close(fd);
584         }
585     }
586 
getContentResolver()587     private ContentResolver getContentResolver() {
588         return getInstrumentation().getContext().getContentResolver();
589     }
590 
isPrivateDnsInStrictMode()591     private boolean isPrivateDnsInStrictMode() {
592         return PRIVATE_DNS_MODE_PROVIDER_HOSTNAME.equals(
593                 Settings.Global.getString(getContentResolver(), PRIVATE_DNS_MODE_SETTING));
594     }
595 
storePrivateDnsSetting()596     private void storePrivateDnsSetting() {
597         mOldPrivateDnsMode = Settings.Global.getString(getContentResolver(),
598                 PRIVATE_DNS_MODE_SETTING);
599         mOldPrivateDnsSpecifier = Settings.Global.getString(getContentResolver(),
600                 PRIVATE_DNS_SPECIFIER_SETTING);
601     }
602 
restorePrivateDnsSetting()603     private void restorePrivateDnsSetting() {
604         Settings.Global.putString(getContentResolver(), PRIVATE_DNS_MODE_SETTING,
605                 mOldPrivateDnsMode);
606         Settings.Global.putString(getContentResolver(), PRIVATE_DNS_SPECIFIER_SETTING,
607                 mOldPrivateDnsSpecifier);
608     }
609 
610     // TODO: replace with CtsNetUtils.awaitPrivateDnsSetting in Q or above.
expectPrivateDnsHostname(final String hostname)611     private void expectPrivateDnsHostname(final String hostname) throws Exception {
612         final NetworkRequest request = new NetworkRequest.Builder()
613                 .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
614                 .build();
615         final CountDownLatch latch = new CountDownLatch(1);
616         final NetworkCallback callback = new NetworkCallback() {
617             @Override
618             public void onLinkPropertiesChanged(Network network, LinkProperties lp) {
619                 if (network.equals(mNetwork) &&
620                         Objects.equals(lp.getPrivateDnsServerName(), hostname)) {
621                     latch.countDown();
622                 }
623             }
624         };
625 
626         mCM.registerNetworkCallback(request, callback);
627 
628         try {
629             assertTrue("Private DNS hostname was not " + hostname + " after " + TIMEOUT_MS + "ms",
630                     latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
631         } finally {
632             mCM.unregisterNetworkCallback(callback);
633         }
634     }
635 
setAndVerifyPrivateDns(boolean strictMode)636     private void setAndVerifyPrivateDns(boolean strictMode) throws Exception {
637         final ContentResolver cr = getInstrumentation().getContext().getContentResolver();
638         String privateDnsHostname;
639 
640         if (strictMode) {
641             privateDnsHostname = "vpncts-nx.metric.gstatic.com";
642             Settings.Global.putString(cr, PRIVATE_DNS_SPECIFIER_SETTING, privateDnsHostname);
643             Settings.Global.putString(cr, PRIVATE_DNS_MODE_SETTING,
644                     PRIVATE_DNS_MODE_PROVIDER_HOSTNAME);
645         } else {
646             Settings.Global.putString(cr, PRIVATE_DNS_MODE_SETTING, PRIVATE_DNS_MODE_OPPORTUNISTIC);
647             privateDnsHostname = null;
648         }
649 
650         expectPrivateDnsHostname(privateDnsHostname);
651 
652         String randomName = "vpncts-" + new Random().nextInt(1000000000) + "-ds.metric.gstatic.com";
653         if (strictMode) {
654             // Strict mode private DNS is enabled. DNS lookups should fail, because the private DNS
655             // server name is invalid.
656             try {
657                 InetAddress.getByName(randomName);
658                 fail("VPN DNS lookup should fail with private DNS enabled");
659             } catch (UnknownHostException expected) {
660             }
661         } else {
662             // Strict mode private DNS is disabled. DNS lookup should succeed, because the VPN
663             // provides no DNS servers, and thus DNS falls through to the default network.
664             assertNotNull("VPN DNS lookup should succeed with private DNS disabled",
665                     InetAddress.getByName(randomName));
666         }
667     }
668 
669     // Tests that strict mode private DNS is used on VPNs.
checkStrictModePrivateDns()670     private void checkStrictModePrivateDns() throws Exception {
671         final boolean initialMode = isPrivateDnsInStrictMode();
672         setAndVerifyPrivateDns(!initialMode);
673         setAndVerifyPrivateDns(initialMode);
674     }
675 
testDefault()676     public void testDefault() throws Exception {
677         if (!supportedHardware()) return;
678         // If adb TCP port opened, this test may running by adb over network.
679         // All of socket would be destroyed in this test. So this test don't
680         // support adb over network, see b/119382723.
681         if (SystemProperties.getInt("persist.adb.tcp.port", -1) > -1
682                 || SystemProperties.getInt("service.adb.tcp.port", -1) > -1) {
683             Log.i(TAG, "adb is running over the network, so skip this test");
684             return;
685         }
686 
687         final BlockingBroadcastReceiver receiver = new BlockingBroadcastReceiver(
688                 getInstrumentation().getTargetContext(), MyVpnService.ACTION_ESTABLISHED);
689         receiver.register();
690 
691         FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
692 
693         startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
694                  new String[] {"0.0.0.0/0", "::/0"},
695                  "", "", null, null /* underlyingNetworks */, false /* isAlwaysMetered */);
696 
697         final Intent intent = receiver.awaitForBroadcast(TimeUnit.MINUTES.toMillis(1));
698         assertNotNull("Failed to receive broadcast from VPN service", intent);
699         assertFalse("Wrong VpnService#isAlwaysOn",
700                 intent.getBooleanExtra(MyVpnService.EXTRA_ALWAYS_ON, true));
701         assertFalse("Wrong VpnService#isLockdownEnabled",
702                 intent.getBooleanExtra(MyVpnService.EXTRA_LOCKDOWN_ENABLED, true));
703 
704         assertSocketClosed(fd, TEST_HOST);
705 
706         checkTrafficOnVpn();
707 
708         checkStrictModePrivateDns();
709 
710         receiver.unregisterQuietly();
711     }
712 
testAppAllowed()713     public void testAppAllowed() throws Exception {
714         if (!supportedHardware()) return;
715 
716         FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
717 
718         // Shell app must not be put in here or it would kill the ADB-over-network use case
719         String allowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName;
720         startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
721                  new String[] {"192.0.2.0/24", "2001:db8::/32"},
722                  allowedApps, "", null, null /* underlyingNetworks */, false /* isAlwaysMetered */);
723 
724         assertSocketClosed(fd, TEST_HOST);
725 
726         checkTrafficOnVpn();
727 
728         checkStrictModePrivateDns();
729     }
730 
testAppDisallowed()731     public void testAppDisallowed() throws Exception {
732         if (!supportedHardware()) return;
733 
734         FileDescriptor localFd = openSocketFd(TEST_HOST, 80, TIMEOUT_MS);
735         FileDescriptor remoteFd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
736 
737         String disallowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName;
738         // If adb TCP port opened, this test may running by adb over TCP.
739         // Add com.android.shell appllication into blacklist to exclude adb socket for VPN test,
740         // see b/119382723.
741         // Note: The test don't support running adb over network for root device
742         disallowedApps = disallowedApps + ",com.android.shell";
743         Log.i(TAG, "Append shell app to disallowedApps: " + disallowedApps);
744         startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
745                  new String[] {"192.0.2.0/24", "2001:db8::/32"},
746                  "", disallowedApps, null, null /* underlyingNetworks */,
747                  false /* isAlwaysMetered */);
748 
749         assertSocketStillOpen(localFd, TEST_HOST);
750         assertSocketStillOpen(remoteFd, TEST_HOST);
751 
752         checkNoTrafficOnVpn();
753     }
754 
testGetConnectionOwnerUidSecurity()755     public void testGetConnectionOwnerUidSecurity() throws Exception {
756         if (!supportedHardware()) return;
757 
758         DatagramSocket s;
759         InetAddress address = InetAddress.getByName("localhost");
760         s = new DatagramSocket();
761         s.setSoTimeout(SOCKET_TIMEOUT_MS);
762         s.connect(address, 7);
763         InetSocketAddress loc = new InetSocketAddress(s.getLocalAddress(), s.getLocalPort());
764         InetSocketAddress rem = new InetSocketAddress(s.getInetAddress(), s.getPort());
765         try {
766             int uid = mCM.getConnectionOwnerUid(OsConstants.IPPROTO_TCP, loc, rem);
767             fail("Only an active VPN app may call this API.");
768         } catch (SecurityException expected) {
769             return;
770         }
771     }
772 
testSetProxy()773     public void testSetProxy() throws  Exception {
774         if (!supportedHardware()) return;
775         ProxyInfo initialProxy = mCM.getDefaultProxy();
776         // Receiver for the proxy change broadcast.
777         BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
778         proxyBroadcastReceiver.register();
779 
780         String allowedApps = mPackageName;
781         ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("10.0.0.1", 8888);
782         startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
783                 new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "",
784                 testProxyInfo, null /* underlyingNetworks */, false /* isAlwaysMetered */);
785 
786         // Check that the proxy change broadcast is received
787         try {
788             assertNotNull("No proxy change was broadcast when VPN is connected.",
789                     proxyBroadcastReceiver.awaitForBroadcast());
790         } finally {
791             proxyBroadcastReceiver.unregisterQuietly();
792         }
793 
794         // Proxy is set correctly in network and in link properties.
795         assertNetworkHasExpectedProxy(testProxyInfo, mNetwork);
796         assertDefaultProxy(testProxyInfo);
797 
798         proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
799         proxyBroadcastReceiver.register();
800         stopVpn();
801         try {
802             assertNotNull("No proxy change was broadcast when VPN was disconnected.",
803                     proxyBroadcastReceiver.awaitForBroadcast());
804         } finally {
805             proxyBroadcastReceiver.unregisterQuietly();
806         }
807 
808         // After disconnecting from VPN, the proxy settings are the ones of the initial network.
809         assertDefaultProxy(initialProxy);
810     }
811 
testSetProxyDisallowedApps()812     public void testSetProxyDisallowedApps() throws Exception {
813         if (!supportedHardware()) return;
814         ProxyInfo initialProxy = mCM.getDefaultProxy();
815 
816         // If adb TCP port opened, this test may running by adb over TCP.
817         // Add com.android.shell appllication into blacklist to exclude adb socket for VPN test,
818         // see b/119382723.
819         // Note: The test don't support running adb over network for root device
820         String disallowedApps = mPackageName + ",com.android.shell";
821         ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("10.0.0.1", 8888);
822         startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
823                 new String[] {"0.0.0.0/0", "::/0"}, "", disallowedApps,
824                 testProxyInfo, null /* underlyingNetworks */, false /* isAlwaysMetered */);
825 
826         // The disallowed app does has the proxy configs of the default network.
827         assertNetworkHasExpectedProxy(initialProxy, mCM.getActiveNetwork());
828         assertDefaultProxy(initialProxy);
829     }
830 
testNoProxy()831     public void testNoProxy() throws Exception {
832         if (!supportedHardware()) return;
833         ProxyInfo initialProxy = mCM.getDefaultProxy();
834         BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
835         proxyBroadcastReceiver.register();
836         String allowedApps = mPackageName;
837         startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
838                 new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null,
839                 null /* underlyingNetworks */, false /* isAlwaysMetered */);
840 
841         try {
842             assertNotNull("No proxy change was broadcast.",
843                     proxyBroadcastReceiver.awaitForBroadcast());
844         } finally {
845             proxyBroadcastReceiver.unregisterQuietly();
846         }
847 
848         // The VPN network has no proxy set.
849         assertNetworkHasExpectedProxy(null, mNetwork);
850 
851         proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
852         proxyBroadcastReceiver.register();
853         stopVpn();
854         try {
855             assertNotNull("No proxy change was broadcast.",
856                     proxyBroadcastReceiver.awaitForBroadcast());
857         } finally {
858             proxyBroadcastReceiver.unregisterQuietly();
859         }
860         // After disconnecting from VPN, the proxy settings are the ones of the initial network.
861         assertDefaultProxy(initialProxy);
862         assertNetworkHasExpectedProxy(initialProxy, mCM.getActiveNetwork());
863     }
864 
testBindToNetworkWithProxy()865     public void testBindToNetworkWithProxy() throws Exception {
866         if (!supportedHardware()) return;
867         String allowedApps = mPackageName;
868         Network initialNetwork = mCM.getActiveNetwork();
869         ProxyInfo initialProxy = mCM.getDefaultProxy();
870         ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("10.0.0.1", 8888);
871         // Receiver for the proxy change broadcast.
872         BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
873         proxyBroadcastReceiver.register();
874         startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
875                 new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "",
876                 testProxyInfo, null /* underlyingNetworks */, false /* isAlwaysMetered */);
877 
878         assertDefaultProxy(testProxyInfo);
879         mCM.bindProcessToNetwork(initialNetwork);
880         try {
881             assertNotNull("No proxy change was broadcast.",
882                 proxyBroadcastReceiver.awaitForBroadcast());
883         } finally {
884             proxyBroadcastReceiver.unregisterQuietly();
885         }
886         assertDefaultProxy(initialProxy);
887     }
888 
testVpnMeterednessWithNoUnderlyingNetwork()889     public void testVpnMeterednessWithNoUnderlyingNetwork() throws Exception {
890         if (!supportedHardware()) {
891             return;
892         }
893         // VPN is not routing any traffic i.e. its underlying networks is an empty array.
894         ArrayList<Network> underlyingNetworks = new ArrayList<>();
895         String allowedApps = mPackageName;
896 
897         startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
898                 new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null,
899                 underlyingNetworks, false /* isAlwaysMetered */);
900 
901         // VPN should now be the active network.
902         assertEquals(mNetwork, mCM.getActiveNetwork());
903         assertVpnTransportContains(NetworkCapabilities.TRANSPORT_VPN);
904         // VPN with no underlying networks should be metered by default.
905         assertTrue(isNetworkMetered(mNetwork));
906         assertTrue(mCM.isActiveNetworkMetered());
907     }
908 
testVpnMeterednessWithNullUnderlyingNetwork()909     public void testVpnMeterednessWithNullUnderlyingNetwork() throws Exception {
910         if (!supportedHardware()) {
911             return;
912         }
913         Network underlyingNetwork = mCM.getActiveNetwork();
914         if (underlyingNetwork == null) {
915             Log.i(TAG, "testVpnMeterednessWithNullUnderlyingNetwork cannot execute"
916                     + " unless there is an active network");
917             return;
918         }
919         // VPN tracks platform default.
920         ArrayList<Network> underlyingNetworks = null;
921         String allowedApps = mPackageName;
922 
923         startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
924                 new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null,
925                 underlyingNetworks, false /*isAlwaysMetered */);
926 
927         // Ensure VPN transports contains underlying network's transports.
928         assertVpnTransportContains(underlyingNetwork);
929         // Its meteredness should be same as that of underlying network.
930         assertEquals(isNetworkMetered(underlyingNetwork), isNetworkMetered(mNetwork));
931         // Meteredness based on VPN capabilities and CM#isActiveNetworkMetered should be in sync.
932         assertEquals(isNetworkMetered(mNetwork), mCM.isActiveNetworkMetered());
933     }
934 
testVpnMeterednessWithNonNullUnderlyingNetwork()935     public void testVpnMeterednessWithNonNullUnderlyingNetwork() throws Exception {
936         if (!supportedHardware()) {
937             return;
938         }
939         Network underlyingNetwork = mCM.getActiveNetwork();
940         if (underlyingNetwork == null) {
941             Log.i(TAG, "testVpnMeterednessWithNonNullUnderlyingNetwork cannot execute"
942                     + " unless there is an active network");
943             return;
944         }
945         // VPN explicitly declares WiFi to be its underlying network.
946         ArrayList<Network> underlyingNetworks = new ArrayList<>(1);
947         underlyingNetworks.add(underlyingNetwork);
948         String allowedApps = mPackageName;
949 
950         startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
951                 new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null,
952                 underlyingNetworks, false /* isAlwaysMetered */);
953 
954         // Ensure VPN transports contains underlying network's transports.
955         assertVpnTransportContains(underlyingNetwork);
956         // Its meteredness should be same as that of underlying network.
957         assertEquals(isNetworkMetered(underlyingNetwork), isNetworkMetered(mNetwork));
958         // Meteredness based on VPN capabilities and CM#isActiveNetworkMetered should be in sync.
959         assertEquals(isNetworkMetered(mNetwork), mCM.isActiveNetworkMetered());
960     }
961 
testAlwaysMeteredVpnWithNullUnderlyingNetwork()962     public void testAlwaysMeteredVpnWithNullUnderlyingNetwork() throws Exception {
963         if (!supportedHardware()) {
964             return;
965         }
966         Network underlyingNetwork = mCM.getActiveNetwork();
967         if (underlyingNetwork == null) {
968             Log.i(TAG, "testAlwaysMeteredVpnWithNullUnderlyingNetwork cannot execute"
969                     + " unless there is an active network");
970             return;
971         }
972         // VPN tracks platform default.
973         ArrayList<Network> underlyingNetworks = null;
974         String allowedApps = mPackageName;
975         boolean isAlwaysMetered = true;
976 
977         startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
978                 new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null,
979                 underlyingNetworks, isAlwaysMetered);
980 
981         // VPN's meteredness does not depend on underlying network since it is always metered.
982         assertTrue(isNetworkMetered(mNetwork));
983         assertTrue(mCM.isActiveNetworkMetered());
984     }
985 
testAlwaysMeteredVpnWithNonNullUnderlyingNetwork()986     public void testAlwaysMeteredVpnWithNonNullUnderlyingNetwork() throws Exception {
987         if (!supportedHardware()) {
988             return;
989         }
990         Network underlyingNetwork = mCM.getActiveNetwork();
991         if (underlyingNetwork == null) {
992             Log.i(TAG, "testAlwaysMeteredVpnWithNonNullUnderlyingNetwork cannot execute"
993                     + " unless there is an active network");
994             return;
995         }
996         // VPN explicitly declares its underlying network.
997         ArrayList<Network> underlyingNetworks = new ArrayList<>(1);
998         underlyingNetworks.add(underlyingNetwork);
999         String allowedApps = mPackageName;
1000         boolean isAlwaysMetered = true;
1001 
1002         startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
1003                 new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null,
1004                 underlyingNetworks, isAlwaysMetered);
1005 
1006         // VPN's meteredness does not depend on underlying network since it is always metered.
1007         assertTrue(isNetworkMetered(mNetwork));
1008         assertTrue(mCM.isActiveNetworkMetered());
1009     }
1010 
testB141603906()1011     public void testB141603906() throws Exception {
1012         final InetSocketAddress src = new InetSocketAddress(0);
1013         final InetSocketAddress dst = new InetSocketAddress(0);
1014         final int NUM_THREADS = 8;
1015         final int NUM_SOCKETS = 5000;
1016         final Thread[] threads = new Thread[NUM_THREADS];
1017         startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
1018                  new String[] {"0.0.0.0/0", "::/0"},
1019                  "" /* allowedApplications */, "com.android.shell" /* disallowedApplications */,
1020                 null /* proxyInfo */, null /* underlyingNetworks */, false /* isAlwaysMetered */);
1021 
1022         for (int i = 0; i < NUM_THREADS; i++) {
1023             threads[i] = new Thread(() -> {
1024                 for (int j = 0; j < NUM_SOCKETS; j++) {
1025                     mCM.getConnectionOwnerUid(IPPROTO_TCP, src, dst);
1026                 }
1027             });
1028         }
1029         for (Thread thread : threads) {
1030             thread.start();
1031         }
1032         for (Thread thread : threads) {
1033             thread.join();
1034         }
1035         stopVpn();
1036     }
1037 
isNetworkMetered(Network network)1038     private boolean isNetworkMetered(Network network) {
1039         NetworkCapabilities nc = mCM.getNetworkCapabilities(network);
1040         return !nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
1041     }
1042 
assertVpnTransportContains(Network underlyingNetwork)1043     private void assertVpnTransportContains(Network underlyingNetwork) {
1044         int[] transports = mCM.getNetworkCapabilities(underlyingNetwork).getTransportTypes();
1045         assertVpnTransportContains(transports);
1046     }
1047 
assertVpnTransportContains(int... transports)1048     private void assertVpnTransportContains(int... transports) {
1049         NetworkCapabilities vpnCaps = mCM.getNetworkCapabilities(mNetwork);
1050         for (int transport : transports) {
1051             assertTrue(vpnCaps.hasTransport(transport));
1052         }
1053     }
1054 
assertDefaultProxy(ProxyInfo expected)1055     private void assertDefaultProxy(ProxyInfo expected) {
1056         assertEquals("Incorrect proxy config.", expected, mCM.getDefaultProxy());
1057         String expectedHost = expected == null ? null : expected.getHost();
1058         String expectedPort = expected == null ? null : String.valueOf(expected.getPort());
1059         assertEquals("Incorrect proxy host system property.", expectedHost,
1060             System.getProperty("http.proxyHost"));
1061         assertEquals("Incorrect proxy port system property.", expectedPort,
1062             System.getProperty("http.proxyPort"));
1063     }
1064 
assertNetworkHasExpectedProxy(ProxyInfo expected, Network network)1065     private void assertNetworkHasExpectedProxy(ProxyInfo expected, Network network) {
1066         LinkProperties lp = mCM.getLinkProperties(network);
1067         assertNotNull("The network link properties object is null.", lp);
1068         assertEquals("Incorrect proxy config.", expected, lp.getHttpProxy());
1069 
1070         assertEquals(expected, mCM.getProxyForNetwork(network));
1071     }
1072 
1073     class ProxyChangeBroadcastReceiver extends BlockingBroadcastReceiver {
1074         private boolean received;
1075 
ProxyChangeBroadcastReceiver()1076         public ProxyChangeBroadcastReceiver() {
1077             super(VpnTest.this.getInstrumentation().getContext(), Proxy.PROXY_CHANGE_ACTION);
1078             received = false;
1079         }
1080 
1081         @Override
onReceive(Context context, Intent intent)1082         public void onReceive(Context context, Intent intent) {
1083             if (!received) {
1084                 // Do not call onReceive() more than once.
1085                 super.onReceive(context, intent);
1086             }
1087             received = true;
1088         }
1089     }
1090 }
1091